溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

關(guān)于并發(fā)框架 Java原生線程池原理及Guava與之的補(bǔ)充

發(fā)布時(shí)間:2020-07-17 00:05:10 來(lái)源:網(wǎng)絡(luò) 閱讀:601 作者:wx5d30212829a35 欄目:編程語(yǔ)言

使用Java中成型的框架來(lái)幫助我們開(kāi)發(fā)并發(fā)應(yīng)用即可以節(jié)省構(gòu)建項(xiàng)目的時(shí)間,也可以提高應(yīng)用的性能。

Java對(duì)象實(shí)例的鎖一共有四種狀態(tài):無(wú)鎖,偏向鎖,輕量鎖和重量鎖。原始脫離框架的并發(fā)應(yīng)用大部分都需要手動(dòng)完成加鎖釋放,最直接的就是使用synchronized和volatile關(guān)鍵字對(duì)某個(gè)對(duì)象或者代碼塊加鎖從而限制每次訪問(wèn)的次數(shù),從對(duì)象之間的競(jìng)爭(zhēng)也可以實(shí)現(xiàn)到對(duì)象之間的協(xié)作。但是這樣手動(dòng)實(shí)現(xiàn)出來(lái)的應(yīng)用不僅耗費(fèi)時(shí)間而且性能表現(xiàn)往往又有待提升。順帶一提,之前寫(xiě)過(guò)一篇文章介紹我基于Qt和Linux實(shí)現(xiàn)的一個(gè)多線程下載器(到這里不需要更多了解這個(gè)下載器,請(qǐng)直接繼續(xù)閱讀),就拿這個(gè)下載器做一次反例:

首先,一個(gè)下載器最愚蠢的問(wèn)題之一就是把下載線程的個(gè)數(shù)交由給用戶去配置。比如一個(gè)用戶會(huì)認(rèn)為負(fù)責(zé)下載的線程個(gè)數(shù)是越多越好,干脆配置了50個(gè)線程去下載一份任務(wù),那么這個(gè)下載器的性能表現(xiàn)甚至?xí)蝗缫粋€(gè)單進(jìn)程的下載程序。最直接的原因就是JVM花費(fèi)了很多計(jì)算資源在線程之間的上下文切換上面,對(duì)于一個(gè)并發(fā)的應(yīng)用:如果是CPU密集型的任務(wù),那么良好的線程個(gè)數(shù)是實(shí)際CPU處理器的個(gè)數(shù)的1倍;如果是I/O密集型的任務(wù),那么良好的線程個(gè)數(shù)是實(shí)際CPU處理器個(gè)數(shù)的1.5倍到2倍(具體記不清這句話是出于哪里了,但還是可信的)。不恰當(dāng)?shù)膱?zhí)行線程個(gè)數(shù)會(huì)給線程抖動(dòng),CPU抖動(dòng)等隱患埋下伏筆。如果,重新開(kāi)發(fā)那么我一定會(huì)使用這種線程池的方法使用生產(chǎn)者和消費(fèi)者的關(guān)系模式,異步處理HTTP傳輸過(guò)來(lái)的報(bào)文。

其次,由于HTTP報(bào)文的接受等待的時(shí)間可能需要等待很久,然而處理報(bào)文解析格式等等消耗的計(jì)算資源是相當(dāng)較小的。同步地處理這兩件事情必然會(huì)使下載進(jìn)程在一段時(shí)間內(nèi)空轉(zhuǎn)或者阻塞,這樣處理也是非常不合理的。如果重新開(kāi)發(fā),一定要解耦HTTP報(bào)文的接收和HTTP報(bào)文的解析,這里盡管也可以使用線程池去進(jìn)行處理,顯而易見(jiàn)由于這樣去做的性能提升其實(shí)是很小的,所以沒(méi)有必要去實(shí)現(xiàn),單線程也可以快速完成報(bào)文的解析。

Okay,回到主題,總而言之是線程之間的上下文切換導(dǎo)致了性能的降低。那么具體應(yīng)該怎么樣去做才可以減少上下文的切換呢?

1. 無(wú)鎖并發(fā)編程

多線程競(jìng)爭(zhēng)鎖時(shí),會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí),可以用一些辦法來(lái)避免使用鎖,如將數(shù)據(jù)的ID按照Hash算法取模分段,不同的線程去處理不同段的數(shù)據(jù)。

2. CAS算法

Java的Atomic包內(nèi)使用CAS算法來(lái)更新數(shù)據(jù),而不需要加鎖(但是線程的空轉(zhuǎn)還是存在)。

3. 使用最少線程

避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建很多線程來(lái)處理,這樣會(huì)造成大量線程都處于等待狀態(tài)。

4. 協(xié)程

在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換。

總的來(lái)說(shuō)使用Java線程池會(huì)帶來(lái)以下3個(gè)好處:

1. 降低資源消耗: 通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。

2. 提高響應(yīng)速度: 當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。

3. 提高線程的可管理性: 線程是稀缺資源,如果無(wú)限制的創(chuàng)建。不僅僅會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以統(tǒng)一分配,調(diào)優(yōu)和監(jiān)控。但是要做到合理的利用線程池。必須對(duì)于其實(shí)現(xiàn)原理了如指掌。

線程池的實(shí)現(xiàn)原理如下圖所示:

關(guān)于并發(fā)框架 Java原生線程池原理及Guava與之的補(bǔ)充


Executor框架的兩級(jí)調(diào)度模型:

在HotSpot VM線程模型中,Java線程被一對(duì)一的映射為本地操作系統(tǒng)線程,Java線程啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)本地操作系統(tǒng)線程,當(dāng)該Java線程終止時(shí),這個(gè)操作系統(tǒng)也會(huì)被回收。操作系統(tǒng)會(huì)調(diào)度并將它們分配給可用的CPU。

在上層,Java多線程程序通常把應(yīng)用分解為若干個(gè)任務(wù),然后把用戶級(jí)的調(diào)度器(Executor框架)將這些映射為固定數(shù)量的線程;在底層,操作系統(tǒng)內(nèi)核將這些線程映射到硬件處理器上。這種兩級(jí)調(diào)度模型實(shí)質(zhì)是一種工作單元和執(zhí)行機(jī)制的解偶。

Fork/Join框架的遞歸調(diào)度模型:

要提高應(yīng)用程序在多核處理器上的執(zhí)行效率,只能想辦法提高應(yīng)用程序的本身的并行能力。常規(guī)的做法就是使用多線程,讓更多的任務(wù)同時(shí)處理,或者讓一部分操作異步執(zhí)行,這種簡(jiǎn)單的多線程處理方式在處理器核心數(shù)比較少的情況下能夠有效地利用處理資源,因?yàn)樵谔幚砥骱诵谋容^少的情況下,讓不多的幾個(gè)任務(wù)并行執(zhí)行即可。但是當(dāng)處理器核心數(shù)發(fā)展很大的數(shù)目,上百上千的時(shí)候,這種按任務(wù)的并發(fā)處理方法也不能充分利用處理資源,因?yàn)橐话愕膽?yīng)用程序沒(méi)有那么多的并發(fā)處理任務(wù)(服務(wù)器程序是個(gè)例外)。所以,只能考慮把一個(gè)任務(wù)拆分為多個(gè)單元,每個(gè)單元分別得執(zhí)行最后合并每個(gè)單元的結(jié)果。一個(gè)任務(wù)的并行拆分,一種方法就是寄希望于硬件平臺(tái)或者操作系統(tǒng),但是目前這個(gè)領(lǐng)域還沒(méi)有很好的結(jié)果。另一種方案就是還是只有依靠應(yīng)用程序本身對(duì)任務(wù)經(jīng)行拆封執(zhí)行。

Fork/Join模型乍看起來(lái)很像借鑒了MapReduce,但是具體不敢肯定是什么原因,實(shí)際用起來(lái)的性能提升是遠(yuǎn)不如Executor的。甚至在遞歸棧到了十層以上的時(shí)候,JVM會(huì)卡死或者崩潰,從計(jì)算機(jī)的物理原理來(lái)看,F(xiàn)ork/Join框架實(shí)際效能也沒(méi)有想象中的那么美好,所以這篇只稍微談一下,不再深究。

Executor框架主要由三個(gè)部分組成:任務(wù),任務(wù)的執(zhí)行,異步計(jì)算的結(jié)果。

主要的類和接口簡(jiǎn)介如下:

1. Executor是一個(gè)接口,它將任務(wù)的提交和任務(wù)的執(zhí)行分離。

2. ThreadPoolExecutor是線程池的核心,用來(lái)執(zhí)行被提交的類。

3. Future接口和實(shí)現(xiàn)Future接口的FutureTask類,代表異步計(jì)算的結(jié)果。

4. Runnable接口和Callable接口的實(shí)現(xiàn)類,都可以被ThreadPoolExecutor或其他執(zhí)行。

先看一個(gè)直接的例子(用SingleThreadExecutor來(lái)實(shí)現(xiàn),具體原理下面會(huì)闡述):

?1?public?class?ExecutorDemo?{
?2?
?3?
?4?public?static?void?main(String[]?args){
?5?
?6?//ExecutorService?fixed=?Executors.newFixedThreadPool(4);
?7?ExecutorService?single=Executors.newSingleThreadExecutor();
?8?//ExecutorService?cached=Executors.newCachedThreadPool();
?9?//ExecutorService?sched=Executors.newScheduledThreadPool(4);
11?
12?Callable<String>?callable=Executors.callable(new?Runnable()?{
13?@Override
14?public?void?run()?{
15?for(int?i=0;i<100;i++){
16?try{
17?System.out.println(i);
18?}catch(Throwable?e){
19?e.printStackTrace();
20?}
21?}
22?}
23?},"success");
24?     //這里抖了個(gè)機(jī)靈,用Executors工具類的callable方法將一個(gè)匿名Runnable對(duì)象裝飾為Callable對(duì)象作為參數(shù)
25?Future<String>?f=single.submit(callable);
26?try?{
27?System.out.println(f.get());
28?single.shutdown();
29?}catch(Throwable?e){
30?e.printStackTrace();
31?}
32?}
33?}

如代碼中所示,常用一共有四種Exector實(shí)現(xiàn)類通過(guò)Executors的工廠方法來(lái)創(chuàng)建Executor的實(shí)例,其具體差別及特點(diǎn)如下所示:

1. FixedThreadPool

這個(gè)是我個(gè)人最常用的實(shí)現(xiàn)類,在Java中最直接的使用方法就是和 Runtime.getRuntime().availableProcessors() 一起使用分配處理器個(gè)數(shù)個(gè)的Executor。內(nèi)部結(jié)構(gòu)大致如下:

關(guān)于并發(fā)框架 Java原生線程池原理及Guava與之的補(bǔ)充


創(chuàng)造實(shí)例的函數(shù)為: Executors.newFixedThreadPool(int nThread);

在JDK1.7里java.util.concurrent包中的源碼中隊(duì)列使用的是new LinkedBlockingQueue<Runnable>,這是一個(gè)×××的隊(duì)列,也就是說(shuō)任務(wù)有可能無(wú)限地積壓在這個(gè)等待隊(duì)列之中,實(shí)際使用是存在一定的隱患。但是構(gòu)造起來(lái)相當(dāng)比較容易,我個(gè)人建議在使用的過(guò)程之中不斷查詢size()來(lái)保證該阻塞隊(duì)列不會(huì)無(wú)限地生長(zhǎng)。

2. SingleThreadExecutor

和 Executors.newFixedThreadPool(1) 完全等價(jià)。

3. CachedThreadPool

和之前兩個(gè)實(shí)現(xiàn)類完全不同的是,這里使用SynchronousQueue替換LinkedBlockingQueue。簡(jiǎn)單提一下SynchronousQueue是一個(gè)沒(méi)有容量的隊(duì)列,一個(gè)offer必須對(duì)應(yīng)一個(gè)poll,當(dāng)然所謂poll操作是由實(shí)際JVM工作線程來(lái)進(jìn)行的,所以對(duì)于使用開(kāi)發(fā)者來(lái)講,這是一個(gè)會(huì)因?yàn)楣ぷ骶€程飽和而阻塞的線程池。(這個(gè)和java.util.concurrent.Exchanger的作用有些相似,但是Exchanger只是對(duì)于兩個(gè)JVM線程的,而SynchronousQueue的阻塞機(jī)制是多個(gè)生產(chǎn)者和多個(gè)消費(fèi)者而言的。)

4. ScheduledThreadPoolExecutor

這個(gè)實(shí)現(xiàn)類內(nèi)部使用的是DelayQueue。DelayQueue實(shí)際上是一個(gè)優(yōu)先級(jí)隊(duì)列的封裝。時(shí)間早的任務(wù)會(huì)擁有更高的優(yōu)先級(jí)。它主要用來(lái)在給定的延遲之后運(yùn)行任務(wù),或者定期執(zhí)行任務(wù)。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor比Timer更加靈活,而且可以有多個(gè)后臺(tái)線程在構(gòu)造函數(shù)之中指定。

Future接口和ListenableFurture接口

Future接口為異步計(jì)算取回結(jié)果提供了一個(gè)存根(stub),然而這樣每次調(diào)用Future接口的get方法取回計(jì)算結(jié)果往往是需要面臨阻塞的可能性。這樣在最壞的情況下,異步計(jì)算和同步計(jì)算的消耗是一致的。Guava庫(kù)中因此提供一個(gè)非常強(qiáng)大的裝飾后的Future接口,使用觀察者模式為在異步計(jì)算完成之后馬上執(zhí)行addListener指定一個(gè)Runnable對(duì)象,從實(shí)現(xiàn)“完成立即通知”。


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI