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

原創博客,轉載請聯繫博主! java

 

 

  轉眼快兩個月沒有更新本身的博客了。算法

  一來感受本身要學的東西仍是太多,與其花幾個小時寫下經驗分享倒不如多看幾點技術書。編程

  二來放眼網上已經有不少成熟的中文文章介紹這些用法,本身贅述無異重造車輪。服務器

  因此,既然開始打算要寫,就但願能夠有一些不同凡響的用法和新意,能夠給你們一點啓發。多線程

 

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

 

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

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

  其次,因爲HTTP報文的接受等待的時間可能須要等待好久,然而處理報文解析格式等等消耗的計算資源是至關較小的。同步地處理這兩件事情必然會使下載進程在一段時間內空轉或者阻塞,這樣處理也是很是不合理的。若是從新開發,必定要解耦HTTP報文的接收和HTTP報文的解析,這裏儘管也可使用線程池去進行處理,顯而易見因爲這樣去作的性能提高實際上是很小的,因此沒有必要去實現,單線程也能夠快速完成報文的解析。ide

 

  Okay,回到主題,總而言之是線程之間的上下文切換致使了性能的下降。那麼具體應該怎麼樣去作才能夠減小上下文的切換呢?函數

 

 


 

 

  1. 無鎖併發編程

    多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不一樣的線程去處理不一樣段的數據。

  2. CAS算法

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

  3. 使用最少線程

    避免建立不須要的線程,好比任務不多,可是建立不少線程來處理,這樣會形成大量線程都處於等待狀態。

  4. 協程

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

 


 

 

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

 

  1. 下降資源消耗:      經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。

  2. 提升響應速度:      當任務到達時,任務能夠不須要等到線程建立就能當即執行。

  3. 提升線程的可管理性:   線程是稀缺資源,若是無限制的建立。不只僅會下降系統的穩定性,使用線程池能夠統一分配,調優和監控。可是要作到合理的利用線程池。必須對於其實現原理了如指掌。

 

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

 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。內部結構大體以下:

 

   創造實例的函數爲:  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對象,從實現「完成當即通知」。這裏提供一個有效的Tutorial :http://ifeve.com/google-guava-listenablefuture/

相關文章
相關標籤/搜索