'關於併發框架 Java原生線程池原理及Guava與之的補充'

"

使用Java中成型的框架來幫助我們開發併發應用即可以節省構建項目的時間,也可以提高應用的性能。

Java對象實例的鎖一共有四種狀態:無鎖,偏向鎖,輕量鎖和重量鎖。原始脫離框架的併發應用大部分都需要手動完成加鎖釋放,最直接的就是使用synchronized和volatile關鍵字對某個對象或者代碼塊加鎖從而限制每次訪問的次數,從對象之間的競爭也可以實現到對象之間的協作。但是這樣手動實現出來的應用不僅耗費時間而且性能表現往往又有待提升。順帶一提,之前寫過一篇文章介紹我基於Qt和Linux實現的一個多線程下載器(到這裡不需要更多瞭解這個下載器,請直接繼續閱讀),就拿這個下載器做一次反例:

首先,一個下載器最愚蠢的問題之一就是把下載線程的個數交由給用戶去配置。比如一個用戶會認為負責下載的線程個數是越多越好,乾脆配置了50個線程去下載一份任務,那麼這個下載器的性能表現甚至會不如一個單進程的下載程序。最直接的原因就是JVM花費了很多計算資源在線程之間的上下文切換上面,對於一個併發的應用:如果是CPU密集型的任務,那麼良好的線程個數是實際CPU處理器的個數的1倍;如果是I/O密集型的任務,那麼良好的線程個數是實際CPU處理器個數的1.5倍到2倍(具體記不清這句話是出於哪裡了,但還是可信的)。不恰當的執行線程個數會給線程抖動,CPU抖動等隱患埋下伏筆。如果,重新開發那麼我一定會使用這種線程池的方法使用生產者和消費者的關係模式,異步處理HTTP傳輸過來的報文。

其次,由於HTTP報文的接受等待的時間可能需要等待很久,然而處理報文解析格式等等消耗的計算資源是相當較小的。同步地處理這兩件事情必然會使下載進程在一段時間內空轉或者阻塞,這樣處理也是非常不合理的。如果重新開發,一定要解耦HTTP報文的接收和HTTP報文的解析,這裡儘管也可以使用線程池去進行處理,顯而易見由於這樣去做的性能提升其實是很小的,所以沒有必要去實現,單線程也可以快速完成報文的解析。

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

1. 無鎖併發編程

多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程去處理不同段的數據。

2. CAS算法

Java的Atomic包內使用CAS算法來更新數據,而不需要加鎖(但是線程的空轉還是存在)。

3. 使用最少線程

避免創建不需要的線程,比如任務很少,但是創建很多線程來處理,這樣會造成大量線程都處於等待狀態。

4. 協程

在單線程裡實現多任務的調度,並在單線程裡維持多個任務間的切換。

總的來說使用Java線程池會帶來以下3個好處:

1. 降低資源消耗: 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

2. 提高響應速度: 當任務到達時,任務可以不需要等到線程創建就能立即執行。

3. 提高線程的可管理性: 線程是稀缺資源,如果無限制的創建。不僅僅會降低系統的穩定性,使用線程池可以統一分配,調優和監控。但是要做到合理的利用線程池。必須對於其實現原理了如指掌。

線程池的實現原理如下圖所示:

"

使用Java中成型的框架來幫助我們開發併發應用即可以節省構建項目的時間,也可以提高應用的性能。

Java對象實例的鎖一共有四種狀態:無鎖,偏向鎖,輕量鎖和重量鎖。原始脫離框架的併發應用大部分都需要手動完成加鎖釋放,最直接的就是使用synchronized和volatile關鍵字對某個對象或者代碼塊加鎖從而限制每次訪問的次數,從對象之間的競爭也可以實現到對象之間的協作。但是這樣手動實現出來的應用不僅耗費時間而且性能表現往往又有待提升。順帶一提,之前寫過一篇文章介紹我基於Qt和Linux實現的一個多線程下載器(到這裡不需要更多瞭解這個下載器,請直接繼續閱讀),就拿這個下載器做一次反例:

首先,一個下載器最愚蠢的問題之一就是把下載線程的個數交由給用戶去配置。比如一個用戶會認為負責下載的線程個數是越多越好,乾脆配置了50個線程去下載一份任務,那麼這個下載器的性能表現甚至會不如一個單進程的下載程序。最直接的原因就是JVM花費了很多計算資源在線程之間的上下文切換上面,對於一個併發的應用:如果是CPU密集型的任務,那麼良好的線程個數是實際CPU處理器的個數的1倍;如果是I/O密集型的任務,那麼良好的線程個數是實際CPU處理器個數的1.5倍到2倍(具體記不清這句話是出於哪裡了,但還是可信的)。不恰當的執行線程個數會給線程抖動,CPU抖動等隱患埋下伏筆。如果,重新開發那麼我一定會使用這種線程池的方法使用生產者和消費者的關係模式,異步處理HTTP傳輸過來的報文。

其次,由於HTTP報文的接受等待的時間可能需要等待很久,然而處理報文解析格式等等消耗的計算資源是相當較小的。同步地處理這兩件事情必然會使下載進程在一段時間內空轉或者阻塞,這樣處理也是非常不合理的。如果重新開發,一定要解耦HTTP報文的接收和HTTP報文的解析,這裡儘管也可以使用線程池去進行處理,顯而易見由於這樣去做的性能提升其實是很小的,所以沒有必要去實現,單線程也可以快速完成報文的解析。

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

1. 無鎖併發編程

多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程去處理不同段的數據。

2. CAS算法

Java的Atomic包內使用CAS算法來更新數據,而不需要加鎖(但是線程的空轉還是存在)。

3. 使用最少線程

避免創建不需要的線程,比如任務很少,但是創建很多線程來處理,這樣會造成大量線程都處於等待狀態。

4. 協程

在單線程裡實現多任務的調度,並在單線程裡維持多個任務間的切換。

總的來說使用Java線程池會帶來以下3個好處:

1. 降低資源消耗: 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

2. 提高響應速度: 當任務到達時,任務可以不需要等到線程創建就能立即執行。

3. 提高線程的可管理性: 線程是稀缺資源,如果無限制的創建。不僅僅會降低系統的穩定性,使用線程池可以統一分配,調優和監控。但是要做到合理的利用線程池。必須對於其實現原理了如指掌。

線程池的實現原理如下圖所示:

關於併發框架 Java原生線程池原理及Guava與之的補充

Executor框架的兩級調度模型:

在HotSpot VM線程模型中,Java線程被一對一的映射為本地操作系統線程,Java線程啟動時會創建一個本地操作系統線程,當該Java線程終止時,這個操作系統也會被回收。操作系統會調度並將它們分配給可用的CPU。

在上層,Java多線程程序通常把應用分解為若干個任務,然後把用戶級的調度器(Executor框架)將這些映射為固定數量的線程;在底層,操作系統內核將這些線程映射到硬件處理器上。這種兩級調度模型實質是一種工作單元和執行機制的解偶。

Fork/Join框架的遞歸調度模型:

要提高應用程序在多核處理器上的執行效率,只能想辦法提高應用程序的本身的並行能力。常規的做法就是使用多線程,讓更多的任務同時處理,或者讓一部分操作異步執行,這種簡單的多線程處理方式在處理器核心數比較少的情況下能夠有效地利用處理資源,因為在處理器核心比較少的情況下,讓不多的幾個任務並行執行即可。但是當處理器核心數發展很大的數目,上百上千的時候,這種按任務的併發處理方法也不能充分利用處理資源,因為一般的應用程序沒有那麼多的併發處理任務(服務器程序是個例外)。所以,只能考慮把一個任務拆分為多個單元,每個單元分別得執行最後合併每個單元的結果。一個任務的並行拆分,一種方法就是寄希望於硬件平臺或者操作系統,但是目前這個領域還沒有很好的結果。另一種方案就是還是隻有依靠應用程序本身對任務經行拆封執行。

Fork/Join模型乍看起來很像借鑑了MapReduce,但是具體不敢肯定是什麼原因,實際用起來的性能提升是遠不如Executor的。甚至在遞歸棧到了十層以上的時候,JVM會卡死或者崩潰,從計算機的物理原理來看,Fork/Join框架實際效能也沒有想象中的那麼美好,所以這篇只稍微談一下,不再深究。

Executor框架主要由三個部分組成:任務,任務的執行,異步計算的結果。

主要的類和接口簡介如下:

1. Executor是一個接口,它將任務的提交和任務的執行分離。

2. ThreadPoolExecutor是線程池的核心,用來執行被提交的類。

3. Future接口和實現Future接口的FutureTask類,代表異步計算的結果。

4. Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或其他執行。

先看一個直接的例子(用SingleThreadExecutor來實現,具體原理下面會闡述):

 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      //這裡抖了個機靈,用Executors工具類的callable方法將一個匿名Runnable對象裝飾為Callable對象作為參數
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實現類通過Executors的工廠方法來創建Executor的實例,其具體差別及特點如下所示:

1. FixedThreadPool

這個是我個人最常用的實現類,在Java中最直接的使用方法就是和 Runtime.getRuntime().availableProcessors() 一起使用分配處理器個數個的Executor。內部結構大致如下:

"

使用Java中成型的框架來幫助我們開發併發應用即可以節省構建項目的時間,也可以提高應用的性能。

Java對象實例的鎖一共有四種狀態:無鎖,偏向鎖,輕量鎖和重量鎖。原始脫離框架的併發應用大部分都需要手動完成加鎖釋放,最直接的就是使用synchronized和volatile關鍵字對某個對象或者代碼塊加鎖從而限制每次訪問的次數,從對象之間的競爭也可以實現到對象之間的協作。但是這樣手動實現出來的應用不僅耗費時間而且性能表現往往又有待提升。順帶一提,之前寫過一篇文章介紹我基於Qt和Linux實現的一個多線程下載器(到這裡不需要更多瞭解這個下載器,請直接繼續閱讀),就拿這個下載器做一次反例:

首先,一個下載器最愚蠢的問題之一就是把下載線程的個數交由給用戶去配置。比如一個用戶會認為負責下載的線程個數是越多越好,乾脆配置了50個線程去下載一份任務,那麼這個下載器的性能表現甚至會不如一個單進程的下載程序。最直接的原因就是JVM花費了很多計算資源在線程之間的上下文切換上面,對於一個併發的應用:如果是CPU密集型的任務,那麼良好的線程個數是實際CPU處理器的個數的1倍;如果是I/O密集型的任務,那麼良好的線程個數是實際CPU處理器個數的1.5倍到2倍(具體記不清這句話是出於哪裡了,但還是可信的)。不恰當的執行線程個數會給線程抖動,CPU抖動等隱患埋下伏筆。如果,重新開發那麼我一定會使用這種線程池的方法使用生產者和消費者的關係模式,異步處理HTTP傳輸過來的報文。

其次,由於HTTP報文的接受等待的時間可能需要等待很久,然而處理報文解析格式等等消耗的計算資源是相當較小的。同步地處理這兩件事情必然會使下載進程在一段時間內空轉或者阻塞,這樣處理也是非常不合理的。如果重新開發,一定要解耦HTTP報文的接收和HTTP報文的解析,這裡儘管也可以使用線程池去進行處理,顯而易見由於這樣去做的性能提升其實是很小的,所以沒有必要去實現,單線程也可以快速完成報文的解析。

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

1. 無鎖併發編程

多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程去處理不同段的數據。

2. CAS算法

Java的Atomic包內使用CAS算法來更新數據,而不需要加鎖(但是線程的空轉還是存在)。

3. 使用最少線程

避免創建不需要的線程,比如任務很少,但是創建很多線程來處理,這樣會造成大量線程都處於等待狀態。

4. 協程

在單線程裡實現多任務的調度,並在單線程裡維持多個任務間的切換。

總的來說使用Java線程池會帶來以下3個好處:

1. 降低資源消耗: 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

2. 提高響應速度: 當任務到達時,任務可以不需要等到線程創建就能立即執行。

3. 提高線程的可管理性: 線程是稀缺資源,如果無限制的創建。不僅僅會降低系統的穩定性,使用線程池可以統一分配,調優和監控。但是要做到合理的利用線程池。必須對於其實現原理了如指掌。

線程池的實現原理如下圖所示:

關於併發框架 Java原生線程池原理及Guava與之的補充

Executor框架的兩級調度模型:

在HotSpot VM線程模型中,Java線程被一對一的映射為本地操作系統線程,Java線程啟動時會創建一個本地操作系統線程,當該Java線程終止時,這個操作系統也會被回收。操作系統會調度並將它們分配給可用的CPU。

在上層,Java多線程程序通常把應用分解為若干個任務,然後把用戶級的調度器(Executor框架)將這些映射為固定數量的線程;在底層,操作系統內核將這些線程映射到硬件處理器上。這種兩級調度模型實質是一種工作單元和執行機制的解偶。

Fork/Join框架的遞歸調度模型:

要提高應用程序在多核處理器上的執行效率,只能想辦法提高應用程序的本身的並行能力。常規的做法就是使用多線程,讓更多的任務同時處理,或者讓一部分操作異步執行,這種簡單的多線程處理方式在處理器核心數比較少的情況下能夠有效地利用處理資源,因為在處理器核心比較少的情況下,讓不多的幾個任務並行執行即可。但是當處理器核心數發展很大的數目,上百上千的時候,這種按任務的併發處理方法也不能充分利用處理資源,因為一般的應用程序沒有那麼多的併發處理任務(服務器程序是個例外)。所以,只能考慮把一個任務拆分為多個單元,每個單元分別得執行最後合併每個單元的結果。一個任務的並行拆分,一種方法就是寄希望於硬件平臺或者操作系統,但是目前這個領域還沒有很好的結果。另一種方案就是還是隻有依靠應用程序本身對任務經行拆封執行。

Fork/Join模型乍看起來很像借鑑了MapReduce,但是具體不敢肯定是什麼原因,實際用起來的性能提升是遠不如Executor的。甚至在遞歸棧到了十層以上的時候,JVM會卡死或者崩潰,從計算機的物理原理來看,Fork/Join框架實際效能也沒有想象中的那麼美好,所以這篇只稍微談一下,不再深究。

Executor框架主要由三個部分組成:任務,任務的執行,異步計算的結果。

主要的類和接口簡介如下:

1. Executor是一個接口,它將任務的提交和任務的執行分離。

2. ThreadPoolExecutor是線程池的核心,用來執行被提交的類。

3. Future接口和實現Future接口的FutureTask類,代表異步計算的結果。

4. Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或其他執行。

先看一個直接的例子(用SingleThreadExecutor來實現,具體原理下面會闡述):

 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      //這裡抖了個機靈,用Executors工具類的callable方法將一個匿名Runnable對象裝飾為Callable對象作為參數
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實現類通過Executors的工廠方法來創建Executor的實例,其具體差別及特點如下所示:

1. FixedThreadPool

這個是我個人最常用的實現類,在Java中最直接的使用方法就是和 Runtime.getRuntime().availableProcessors() 一起使用分配處理器個數個的Executor。內部結構大致如下:

關於併發框架 Java原生線程池原理及Guava與之的補充

創造實例的函數為: Executors.newFixedThreadPool(int nThread);

在JDK1.7裡java.util.concurrent包中的源碼中隊列使用的是new LinkedBlockingQueue<Runnable>,這是一個無界的隊列,也就是說任務有可能無限地積壓在這個等待隊列之中,實際使用是存在一定的隱患。但是構造起來相當比較容易,我個人建議在使用的過程之中不斷查詢size()來保證該阻塞隊列不會無限地生長。

2. SingleThreadExecutor

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

3. CachedThreadPool

和之前兩個實現類完全不同的是,這裡使用SynchronousQueue替換LinkedBlockingQueue。簡單提一下SynchronousQueue是一個沒有容量的隊列,一個offer必須對應一個poll,當然所謂poll操作是由實際JVM工作線程來進行的,所以對於使用開發者來講,這是一個會因為工作線程飽和而阻塞的線程池。(這個和java.util.concurrent.Exchanger的作用有些相似,但是Exchanger只是對於兩個JVM線程的,而SynchronousQueue的阻塞機制是多個生產者和多個消費者而言的。)

4. ScheduledThreadPoolExecutor

這個實現類內部使用的是DelayQueue。DelayQueue實際上是一個優先級隊列的封裝。時間早的任務會擁有更高的優先級。它主要用來在給定的延遲之後運行任務,或者定期執行任務。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor比Timer更加靈活,而且可以有多個後臺線程在構造函數之中指定。

Future接口和ListenableFurture接口

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

"

相關推薦

推薦中...