Java多執行緒——執行緒池

gary-liu發表於2017-03-12

執行緒池

執行緒池負責管理工作執行緒,包含一個等待執行的任務佇列。執行緒池的任務佇列是一個Runnable集合,工作執行緒負責從任務佇列中取出並執行Runnable物件。Executor 框架便是 Java 5 中引入的,其內部使用了執行緒池機制,Executor 框架包括:執行緒池,Executor,Executors,ExecutorService,CompletionService,Future,Callable 等。

單執行緒的弊端

  1. 每次new Thread新建物件效能差。

  2. 執行緒缺乏統一管理,可能無限制新建執行緒,相互之間競爭,及可能佔用過多系統資源導致當機或 oom。

  3. 缺乏更多功能,如定時執行、定期執行、執行緒中斷。

不控制執行緒數量,不斷建立新執行緒,很快會導致oom,執行緒還是很佔用資源的,執行緒棧的大小,JDK5.0以後每個執行緒堆疊大小預設為1M,以前每個執行緒堆疊大小為256K;可以通過jvm引數-Xss來設定;注意-Xss是jvm的非標準引數,不強制所有平臺的jvm都支援。

執行緒池的優點

  1. 降低資源消耗。使用執行緒池的好處是重用存在的執行緒,減少在建立和銷燬執行緒上所花的時間以及系統資源的開銷,如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者 “過度切換”的問題。

  2. 提高響應速度。重用存在的執行緒,任務可以不需要等到執行緒建立就能立即執行。

  3. 提高執行緒的可管理性。提供定時執行、定期執行、單執行緒、併發數控制等功能,執行緒池可以進行統一的分配,調優和監控。

執行緒池分類

Executors 提供了一系列工廠方法用於創先執行緒池,返回的執行緒池都實現了 ExecutorService 介面。

  1. newCachedThreadPool:建立一個可快取的執行緒池,呼叫execute將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。快取型池子通常用於執行一些生存期很短的非同步型任務 因此在一些面向連線的 daemon 型 SERVER 中用得不多。但對於生存期短的非同步任務,它是 Executor 的首選。不限制執行緒數,可能會導致oom。

  2. newFixedThreadPool: 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。

  3. newScheduledThreadPool: 建立一個定長執行緒池,支援定時及週期性任務執行,多數情況下可用來替代Timer類。

  4. newSingleThreadExecutor: 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

核心類 ThreadPoolExecutor

java.uitl.concurrent.ThreadPoolExecutor 類是執行緒池中最核心的一個類,有四個構造方法,拿一個構造方法舉例說下引數的意思。

 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
  1. corePoolSize:核心池的大小,這個引數跟後面講述的執行緒池的實現原理有非常大的關係。在建立了執行緒池後,預設情況下,執行緒池中並沒有任何執行緒,而是等待有任務到來才建立執行緒去執行任務,除非呼叫了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預建立執行緒的意思,即在沒有任務到來之前就建立corePoolSize個執行緒或者一個執行緒。預設情況下,在建立了執行緒池後,執行緒池中的執行緒數為0,當有任務來之後,就會建立一個執行緒去執行任務,當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到快取佇列當中;

  2. keepAliveTime:表示執行緒沒有任務執行時最多保持多久時間會終止。預設情況下,只有當執行緒池中的執行緒數大於corePoolSize時,keepAliveTime才會起作用,直到執行緒池中的執行緒數不大於corePoolSize,即當執行緒池中的執行緒數大於corePoolSize時,如果一個執行緒空閒的時間達到keepAliveTime,則會終止,直到執行緒池中的執行緒數不超過corePoolSize。但是如果呼叫了allowCoreThreadTimeOut(boolean)方法,線上程池中的執行緒數不大於corePoolSize時,keepAliveTime引數也會起作用,直到執行緒池中的執行緒數為0;

  3. workQueue:一個阻塞佇列,用來儲存等待執行的任務。ArrayBlockingQueue 和 PriorityBlockingQueue 使用較少,一般使用 LinkedBlockingQueue , newCachedThreadPool 使用的 SynchronousQueue 。執行緒池的排隊策略與 BlockingQueue 有關。

  4. threadFactory:執行緒工廠,主要用來建立執行緒;

  5. handler:當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。有以下四種取值:

ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常(預設採取的策略)。 
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務 

執行緒池執行任務方法

execute()

方法實際上是Executor中宣告的方法,在ThreadPoolExecutor進行了具體的實現,這個方法是ThreadPoolExecutor的核心方法,通過這個方法可以向執行緒池提交一個任務,交由執行緒池去執行。但是execute方法沒有返回值,所以無法判斷任務是否被執行緒池執行成功。

submit()

方法是在ExecutorService中宣告的方法,在AbstractExecutorService就已經有了具體的實現,在ThreadPoolExecutor中並沒有對其進行重寫,這個方法也是用來向執行緒池提交任務的,但是它和execute()方法不同,它能夠返回任務執行的結果,submit()執行 Callable 任務,會發現它實際上還是呼叫的execute()方法,利用了Future來獲取任務執行結果。(程式碼示例參見:http://wiki.jikexueyuan.com/project/java-concurrency/executor.html

執行緒池關閉

我們可以通過呼叫執行緒池的shutdown或shutdownNow方法來關閉執行緒池,它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt 方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow (暴力關閉) 首先將執行緒池的狀態設定成 STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而 shutdown (平緩關閉) 只是將執行緒池的狀態設定成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法的其中一個,isShutdown() 方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed() 方法會返回 true。至於我們應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫 shutdown 來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow。

shutdown()方法使執行緒池處於 SHUTDOWN 狀態,此時執行緒池不能夠接受新的任務,它會等待所有任務執行完畢;

shutdownNow() 方法使執行緒池處於STOP狀態,此時執行緒池不能接受新的任務,並且會去嘗試終止正在執行的任務;

當執行緒池處於 SHUTDOWN 或 STOP 狀態,並且所有工作執行緒已經銷燬,任務快取佇列已經清空或執行結束後,執行緒池被設定為TERMINATED 狀態。

通過類 ThreadPoolExecutor 執行緒執行的原始碼分析可以參考文章:http://www.cnblogs.com/dolphin0520/p/3932921.html

執行緒池監控

ThreadPoolExecutor 提供了一些方法,可以檢視執行狀態、執行緒池大小、活動執行緒數和任務數。

 getPoolSize()  執行緒池的執行緒數量。
 getCorePoolSize() 執行緒池基本執行緒數。
 getActiveCount() 活躍執行緒數
 getCompletedTaskCount() 獲取完成任務數
 getTaskCount()   計劃要執行任務數,不一定準確
 isShutdown()  執行緒池的狀態是否是shutdown
 isTerminated()  所有任務是否都執行完畢

通過擴充套件執行緒池進行監控。通過繼承執行緒池並重寫執行緒池的beforeExecute,afterExecute 和 terminated 方法,我們可以在任務執行前,執行後和執行緒池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法線上程池裡是空方法。

執行緒池執行任務流程

  1. 如果當前執行緒池中的執行緒數目小於 corePoolSize,則每來一個任務,就會建立一個執行緒去執行這個任務;

  2. 如果當前執行緒池中的執行緒數目已經等於 corePoolSize,則每來一個任務,會嘗試將其新增到任務緩衝佇列當中,若新增成功,則該任務會等待空閒執行緒將其取出去執行;若新增失敗,一般來說是任務緩衝佇列已滿。如果緩衝佇列已滿並且當前執行緒數小於 maximumPoolSize,則會嘗試建立新的執行緒去執行這個任務;

  3. 如果當前執行緒池中的執行緒數目達到 maximumPoolSize,則會採取任務拒絕策略進行處理;

  4. 如果執行緒池中的執行緒數量大於 corePoolSize時,如果某執行緒空閒時間超過 keepAliveTime,執行緒將被終止,直至執行緒池中的執行緒數目不大於 corePoolSize;如果允許為核心池中的執行緒設定存活時間,那麼核心池中的執行緒空閒時間超過 keepAliveTime,執行緒也會被終止。

執行緒池任務佇列排隊策略

workQueue的型別為BlockingQueue,通常可以取下面三種型別:

  1. ArrayBlockingQueue:基於陣列的先進先出佇列,此佇列建立時必須指定大小;有助於防止資源耗盡,但是可能較難調整和控制,佇列大小和最大池大小需要相互折衷,需要設定合理的引數。
      
  2. LinkedBlockingQueue:基於連結串列的先進先出佇列,如果建立時沒有指定此佇列大小,則預設為Integer.MAX_VALUE;
      
  3. synchronousQueue:這個佇列比較特殊,它不會儲存提交的任務,而是將直接新建一個執行緒來執行新來的任務。newCachedThreadPool 使用該佇列處理任務。

合理配置執行緒池的大小

獲取cpu核數:Runtime.getRuntime().availableProcessors();

一般需要根據任務的型別來配置執行緒池大小:

如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為 NCPU+1
如果是IO密集型任務,參考值可以設定為2*NCPU

當然,這只是一個參考值,具體的設定還需要根據實際情況進行調整,比如可以先將執行緒池大小設定為參考值,再觀察任務執行情況和系統負載、資源利用率來進行適當調整。

可能出現的問題

下面這段引用來自阿里的java開發規範

>強制執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣 的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。 說明:Executors 返回的執行緒池物件的弊端如下: 
(1)FixedThreadPool 和 SingleThreadPool:
允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM. (2) CachedThreadPool 和 ScheduledThreadPool:
允許的建立執行緒數量為 Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。

建議使用有界佇列,有界佇列能增加系統的穩定性和預警能力,可以根據需要設大一點,比如幾千。有一次我們組使用的後臺任務執行緒池的佇列和執行緒池全滿了,不斷的丟擲拋棄任務的異常,通過排查發現是資料庫出現了問題,導致執行SQL變得非常緩慢,因為後臺任務執行緒池裡的任務全是需要向資料庫查詢和插入資料的,所以導致執行緒池裡的工作執行緒全部阻塞住,任務積壓線上程池裡。如果當時我們設定成無界佇列,執行緒池的佇列就會越來越多,有可能會撐滿記憶體,導致整個系統不可用,而不只是後臺任務出現問題。當然我們的系統所有的任務是用的單獨的伺服器部署的,而我們使用不同規模的執行緒池跑不同型別的任務,但是出現這樣問題時也會影響到其他任務。

參考資料

JAVA THREAD POOL
Java併發程式設計:執行緒池的使用(很細緻的好文章)
聊聊併發(三)——JAVA執行緒池的分析和使用

相關文章