Java并發(fā)編程(3)- FutureTask詳解與池化思想的設(shè)計(jì)和實(shí)戰(zhàn)二

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。


Java并發(fā)編程領(lǐng)域,FutureTask可以說(shuō)是一個(gè)非常強(qiáng)大的利器,它通過(guò)實(shí)現(xiàn)RunnableFuture接口間接擁有了RunnableFuture接口的相關(guān)特性,既可以用于充當(dāng)線程執(zhí)行的任務(wù)(Runnable),也可以用于獲取線程異步執(zhí)行任務(wù)后返回的結(jié)果(Future);本文將基于FutureTask實(shí)戰(zhàn)一個(gè)高級(jí)案例:設(shè)計(jì)一款簡(jiǎn)化版的池容器,以此學(xué)習(xí)鞏固池化思想. 

寫(xiě)在前面的話:debug最近又出了一本新書(shū):Spring Boot企業(yè)級(jí)項(xiàng)目-入門到精通》感興趣的小伙伴可以前往各大商城平臺(tái)(淘寶、天貓、當(dāng)當(dāng)、京東等)一睹為快!書(shū)籍的封面如下所示,后續(xù)debug會(huì)專門出篇文章專門介紹這本書(shū)(同時(shí)提供優(yōu)惠購(gòu)書(shū)渠道):   


言歸正傳,在上篇文章中:Java并發(fā)編程(2)-FutureTask詳解與池化思想的設(shè)計(jì)和實(shí)戰(zhàn)一,我們已經(jīng)從源碼的角度結(jié)合多線程ThreadPoolExecuto,深入剖析并解讀了FutureTask 的相關(guān)API,從任務(wù)的創(chuàng)建、到任務(wù)的執(zhí)行 最后再到 線程執(zhí)行完任務(wù)后異步獲取執(zhí)行結(jié)果;整個(gè)過(guò)程下來(lái),想必各位看官老爺們應(yīng)該收獲頗豐。

而本文我們將趁熱打鐵,進(jìn)一步介紹FutureTask在實(shí)際項(xiàng)目開(kāi)發(fā)中的作用;總的來(lái)說(shuō),FutureTask在實(shí)際項(xiàng)目開(kāi)發(fā)中起到的作用有兩個(gè):

(一)FutureTask執(zhí)行多任務(wù)計(jì)算的場(chǎng)景

比如網(wǎng)站“程序員實(shí)戰(zhàn)基地fightjava.com”的首頁(yè)的數(shù)據(jù)是由多個(gè)功能模塊組成的:輪播圖、最新課程、最新博客、最新學(xué)習(xí)路線、最新資料、友情鏈接等模塊數(shù)據(jù);

這些模塊由于具有獨(dú)立性、互不相關(guān)性,因此可以開(kāi)啟多個(gè)FutureTask,然后交由多線程去執(zhí)行,最終再統(tǒng)一通過(guò)get()方法獲取多線程異步執(zhí)行任務(wù)的結(jié)果返回給前端瀏覽器(這一場(chǎng)景在debug最新擼的課程:Java工程師核心技術(shù)-典型案例與面試實(shí)戰(zhàn)系列二 就有重點(diǎn)介紹過(guò),感興趣的小伙伴可以前往觀看學(xué)習(xí)?。?span lang="EN-US">


(二)在高并發(fā)場(chǎng)景下確保任務(wù)只被執(zhí)行一次

在很多高并發(fā)的場(chǎng)景下,往往我們只需要某些任務(wù)被執(zhí)行一次,這種情景FutureTask就能勝任(當(dāng)然啦,可能還要借助像ConcurrentHashMap這樣的組件輔助;而本文要介紹的便是在這一場(chǎng)景下FutureTask所發(fā)揮的作用!


按照慣例,我們還是先來(lái)介紹下這一場(chǎng)景/需求吧:假設(shè)有一個(gè)帶Key的連接池,當(dāng)Key存在時(shí),則直接返回Key對(duì)應(yīng)的連接對(duì)象;當(dāng)Key不存在時(shí),則創(chuàng)建一個(gè) “連接”對(duì)象;


對(duì)于這樣的應(yīng)用場(chǎng)景,通常采用的方法是使用一個(gè)Map來(lái)存儲(chǔ)Key和連接池對(duì)應(yīng)的對(duì)應(yīng)關(guān)系,而由于這是出于高并發(fā)的應(yīng)用場(chǎng)景,因此穩(wěn)妥的方式是采用ConcurrentHashMap,下面debug將采用N種方式對(duì)此進(jìn)行實(shí)現(xiàn),當(dāng)然啦,最后的實(shí)現(xiàn)方式當(dāng)然是FutureTask啦,畢竟大佬總是最后才出場(chǎng)的?。。?span lang="EN-US">


 

話不多說(shuō),進(jìn)入代碼實(shí)戰(zhàn)環(huán)節(jié)

1)首先出場(chǎng)的是傳統(tǒng)的實(shí)現(xiàn)方式,即按照常規(guī)的業(yè)務(wù)邏輯、算法實(shí)現(xiàn)的方式:   

@Component
@Slf4j
public class MyConnectionPool {
private ConcurrentHashMap<String,MyConnection> connMap=new ConcurrentHashMap<>();

//獲取鏈接
public MyConnection getConn(final String key){
MyConnection conn=connMap.get(key);
if (conn!=null){
return conn;
}
conn=createConn(key);
connMap.putIfAbsent(key,conn);
return conn;
}
}

代碼的含義應(yīng)該不難理解哈,這但凡有點(diǎn)英語(yǔ)基礎(chǔ)的閉著眼睛都能猜出來(lái)是啥意思,除非你英語(yǔ)是體育老師教的:

 

那么到底這種方式行不行呢?其實(shí)行不行并不是由你我說(shuō)了算,而是得需要先經(jīng)過(guò)壓測(cè)哈,因?yàn)槲覀兩厦嬉呀?jīng)說(shuō)了,必須要滿足“高并發(fā)環(huán)境”的前提;OK,啪的一下打開(kāi)JMeter.sh,然后設(shè)置QPS=1000,甚至10000 ,很快啊,如下圖所示:


 

從上圖中就可以看出,此種方式雖然最終是可以得到想要的結(jié)果,但是卻產(chǎn)生了大量的、很有可能會(huì)被閑置的連接資源,因此這種方式不值得推薦!


2)第二種要出場(chǎng)的是“synchronized”,其實(shí)現(xiàn)代碼如下所示:   

public synchronized MyConnection getConnA(final String key){
MyConnection conn=connMap.get(key);
if (conn!=null){
return conn;
}
conn=createConn(key);
connMap.putIfAbsent(key,conn);
return conn;
}

相對(duì)于第一種方式,雖然解決了安全性問(wèn)題,但是卻大大犧牲了性能,無(wú)法提高前端的并發(fā)量;即 synchronized這種同步方式,雖然犧牲了性能,但卻沒(méi)有浪費(fèi)由于大量創(chuàng)建的連接所占用的空間資源(這其實(shí)是一種加  獨(dú)占/悲觀 鎖的方式)

JMeter一通壓測(cè)下來(lái),發(fā)現(xiàn)確實(shí)沒(méi)啥問(wèn)題哈,如下圖所示:


3)第三種實(shí)現(xiàn)方式仍然是加鎖,只是在這里加的是ZooKeeper的分布式鎖,話不多說(shuō),直接上代碼:   

//基于zk的分布式鎖 ~ 這種就集成、依賴了第三方的中間件(不具備獨(dú)立對(duì)外提供服務(wù)的特性)
public MyConnection getConnB(final String key) throws Exception{
MyConnection conn=null;
conn=connMap.get(key);
if (conn!=null){
return conn;
}
//操作zookeeper的客戶端實(shí)例
InterProcessMutex mutex=new InterProcessMutex(client,"/pool/V4");
try {
if (mutex.acquire(4,TimeUnit.SECONDS)){
conn=connMap.get(key);
if (conn!=null){
return conn;
}
conn=createConn(key);
connMap.putIfAbsent(key,conn);
}
}catch (Exception e){
}finally {
mutex.release();
}
return conn;
}

對(duì)于ZooKeeper不熟悉的小伙伴可以前往 “程序員實(shí)戰(zhàn)基地fightjava.com”觀看debug以前擼過(guò)的課程,比如“分布式鎖實(shí)戰(zhàn)視頻教程(基于Spring Boot”等等。上述的實(shí)現(xiàn)方式經(jīng)過(guò)壓測(cè)后也沒(méi)有啥問(wèn)題:



雖然在高并發(fā)的場(chǎng)景下性能是沒(méi)得說(shuō)的、結(jié)果也是正確的;但是呢,它的缺陷也是很明顯的,那就是集成、依賴了第三方的中間件ZooKeeper,不具備獨(dú)立對(duì)外提供服務(wù)的特性,對(duì)外不友好 (所謂的“獨(dú)立”,指的是最好能直接基于JDK、而不依賴于任何第三方組件,且可移植性良好的特點(diǎn))

 

4)最后要登場(chǎng)的自然是FutureTask啦,老規(guī)矩,還是先上代碼哈:   

private ConcurrentHashMap<String,FutureTask<MyConnection>> connHashMap=new ConcurrentHashMap<>();

//基于futureTask
public MyConnection getConnC(final String key) throws Exception{
FutureTask<MyConnection> futureTask=connHashMap.get(key);
if (futureTask!=null){
return futureTask.get();
}

//多線程訪問(wèn)這段代碼都可以執(zhí)行,即都創(chuàng)建了 “獲取連接” 的任務(wù),
//但是注意:此時(shí)還沒(méi)真正地執(zhí)行 獲取連接對(duì)象實(shí)例 的代碼
Callable<MyConnection> callable= () -> createConn(key);
FutureTask<MyConnection> newTask= new FutureTask<>(callable);

//同一時(shí)刻,對(duì)于同個(gè)key并發(fā)的多個(gè)線程只有一個(gè)可以成功此行代碼,即 putIfAbsent
//當(dāng)返回null時(shí)表示現(xiàn)在本地映射map還沒(méi)有該key對(duì)應(yīng)的task,否則調(diào)取get()方法
//堵塞式等待獲取執(zhí)行結(jié)果即可
futureTask=connHashMap.putIfAbsent(key,newTask);
if (futureTask==null){
futureTask=newTask;
futureTask.run();
}
return futureTask.get();
}


JMeter壓測(cè)過(guò)后,觀察最終的結(jié)果,發(fā)現(xiàn)也是沒(méi)啥問(wèn)題的,如下圖所示:


 

有小伙伴可能會(huì)問(wèn),這是為啥呢?為啥這種方式也可行呢?這主要有兩個(gè)原因:

A. ConcurrentHashMap<String,FutureTask<MyConnection>> 該方法從名字上就可以看出大概的意思:適用于高并發(fā)下的場(chǎng)景,它的Key具有唯一性,而且putIfAbsent() 方法的作用在于同一時(shí)間只會(huì)有一個(gè)線程執(zhí)行該方法成功,當(dāng)返回null時(shí),表示還不存在該key,否則,表示已經(jīng)存在該key了,你再put進(jìn)去也沒(méi)用了,某種程度上,這其實(shí)也是一個(gè)加鎖的過(guò)程(Redis SETEX 也正有此種功效)

 

B.建立在A的基礎(chǔ)上,只要保證FutureTask中任務(wù)的“創(chuàng)建”早于ConcurrentHashMapputIfAbsent()方法、而真正“執(zhí)行其真正的代碼邏輯”時(shí)則晚于ConcurrentHashMapputIfAbsent()方法即可,之所以要如此做,正是因?yàn)?span lang="EN-US"> A 中提到的 putIfAbsent() 起到了加鎖的作用,同一時(shí)間將只會(huì)有一個(gè)線程趟過(guò)去,牛逼吧:


 

OK,至此,我們也就擼完了,收工!咱們下期再見(jiàn)?。?!

總結(jié)

1)代碼下載:文章涉及到的代碼可以通過(guò)關(guān)注“程序員實(shí)戰(zhàn)基地”微信公眾號(hào)(掃描下圖微信公眾號(hào)即可),回復(fù)數(shù)字:100,即可獲取代碼下載鏈接:

我是debug,一個(gè)相信技術(shù)改變生活、技術(shù)成就夢(mèng)想 的攻城獅;如果本文對(duì)你有幫助,歡迎你關(guān)注debug的技術(shù)公眾號(hào)一起學(xué)習(xí)干貨技術(shù),并動(dòng)動(dòng)手指點(diǎn)贊、收藏以及轉(zhuǎn)發(fā),你的三連可是debug分享的動(dòng)力哦