ThreadPoolExecutor使用+工作機理+生命週期

原文地址:http://www.cnblogs.com/java-zhao/p/5146601.html

1、最基礎的線程池ThreadPoolExecutor

使用方式:

 1 /**
 2  * ThreadPoolExecutor測試類
 3  * 注意:
 4  * 1、ThreadPoolExecutor是一個線程池
 5  * 2、多個任務都可以由該線程池中選出幾條線程來執行
 6  */
 7 public class ThreadPoolExecutorTest {
 8     private static ThreadPoolExecutor executor = 
 9             new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
10     
11     public void executeTask(){
12         Task1 task1 = new Task1();//構建任務1
13         Task2 task2 = new Task2();//構建任務2
14         executor.execute(task1);//執行任務1
15         executor.execute(task2);//執行任務2
16     }
17     
18     /*
19      * 基本任務2
20      */
21     class Task1 implements Runnable{
22         public void run() {
23             //具體任務的業務
24             for(int i=0;i<1000;i++){
25                 System.out.println("hello xxx!!!");
26             }
27         }
28     }
29     
30     /*
31      * 基本任務2
32      */
33     class Task2 implements Runnable{
34         public void run() {
35             //具體任務的業務
36             for(int i=0;i<5;i++){
37                 System.out.println("hello world2!!!");
38             }
39         }
40     }
41     
42     public static void main(String[] args) {
43         ThreadPoolExecutorTest test = new ThreadPoolExecutorTest();
44         test.executeTask();
45     }
46 }

說明:

在代碼中,構建了一個線程池(executor)和兩個實現了Runnable接口的任務(task1、task2),並將這兩個任務提交到executor中去執行。

線程池的配置:集合下邊的工作機理與參數詳細說明來說。

當然,上述的執行結果是交叉着的,因爲存在線程的切換。

 

2、工作機理

A、當一個新的任務被提交到ThreadPoolExecutor的execute()方法中時,如果當前池中正在運行的線程少於corePoolSize,則會創建一個新的線程來處理該任務;

注意:這是池中正在運行的線程,爲什麼這樣說呢?是因爲核心線程是每來一個任務才創建一個線程,這個看第三部分。看完第三部分之後,你就會覺得,其實換個說法:"如果當前池中的線程少於corePoolSize"這樣會更準確,因爲我們也許會通過下邊介紹的方法提前將核心線程創建好,如果假設這時候來了一個任務,而所有的核心線程都處於空閒狀態的話,這時候是不會去創建新線程的。

B、如果當前池中的線程大於等於corePoolSize,但是小於maximumPoolSize時,如果隊列滿了,會創建新的線程來處理任務,如果隊列沒有滿,任務加入到隊列中去;

C、如果隊列滿了,正在運行的線程數已經等於maximumPoolSize時,該任務就會被rejected(回絕)

 

3、參數詳細說明

A、corePoolSize與maximumPoolSize

  • 如果corePoolSize==maximumPoolSize,線程池的size就是固定的了(這一塊兒類似於堆內存的指定,防止擴張帶來的損耗,但要視情況而定);
  • 默認情況下,只有當一個新的任務到達時,纔會創建和啓動core threads,但是可以通過prestartCoreThread和prestartAllCoreThreads來改變;

B、ThreadFactory

  • 通過使用java.util.concurrent.ThreadFactory可以創建新的線程
  • 如果不額外指定ThreadFactory,則使用默認的Executors#defaultThreadFactory
  • 通過該默認的線程工廠,所有創建的線程都會被加入到同一個ThreadGroup中去,並且這些線程都會有相同的優先級(NORM_PRIORITY),並且都是non-daemon線程

注意:這一塊兒有一個後臺(daemon)線程的概念,典型的後臺線程:垃圾回收線程;這個線程與其他應用線程的不同之處在於:當所有的應用線程都沒有後,後臺線程也就自動消失了。

C、keepAliveTime

  • 如果pool當前擁有的線程超過了corePoolSize,超出的線程如果在大於keepAliveTime的時間外閒置(idle),這些線程就會被終止;
  • 該機制在pool沒有被活躍的使用的時候,可以減少資源浪費;
  • 默認情況下,keep-alive機制僅僅會在線程數超過corePoolSizeThreads時纔會被使用;
  • 當然,通過使用ThreadPoolExecutor#allowCoreThreadTimeOut(boolean)也可以將這種keep-alive機制應用在core threads上去(只要keepAliveTime>0即可)

D、Queue

任何一種BlockingQueue都可以被用來傳遞和存儲提交到線程池中的任務,有三種隊列策略:

1)SynchronousQueue(默認)

  • 直接將任務移交給線程而不是入隊,如果已經沒有線程立即來處理提交到pool中的任務時,會創建一個新的線程來處理該任務;
  • 這種策略需要maximumPoolSizes無界來確保新提交的任務不會被rejection;
  • 這種方式的最大缺點:當任務到來的速度大於任務被處理的速度時,線程數會瘋長。

2)無界隊列LinkedBlockingQueue:

  • 由於隊列無界,當運行的線程等於corePoolSize時,新到來的任務會入隊而不會創建新的線程來執行(即pool中的線程數永遠不會大於corePoolSize);
  • 這種方式的缺點:當任務到來的速度大於任務被處理的速度時,隊列長度會瘋長。

3)有界隊列ArrayBlockingQueue:

  • 這種方式是非常難處理好的一種方式,要考慮好ArrayBlockingQueue的大小和maximumPoolSize的大小;
  • 當ArrayBlockingQueue較大而maximumPoolSize較小時,會降低CPU使用率、減少OS資源、減少上下文切換,但是吞吐量會降低。-->線程較少的特點就是這樣;
  • 如果任務頻繁的被阻塞(例如,they are I/O bound),就需要更多的線程了;
  • 當ArrayBlockingQueue較小而maximumPoolSize較大時,會使CPU使用繁忙但也會遇到一些不可接受的scheduling,吞吐量也會降低。

說明:這一塊兒配置是一個比較麻煩的地方,後邊會說。

E、回絕任務

執行回絕的場景:看開頭部分的工作機理。

在回絕任務的時候,execute()方法會調用RejectedExecutionHandler#rejectedExecution。有四種handler策略:

1)ThreadPoolExecutor.CallerRunsPolicy:調用execute()的線程自己來處理該任務,絕大部分情況下是主線程。

注意:由於主線程執行這個任務,那麼新到來的任務就不會被提交到線程池中執行(而是提交到TCP層的隊列,TCP層隊列滿了,就開始拒絕,此時性能已經很低了),直到主線程執行完這個任務。

2)ThreadPoolExecutor.DiscardPolicy:不能被執行的任務會直接被扔掉

3)ThreadPoolExecutor.DiscardOldestPolicy:如果executor沒有被關閉,隊列頭部的任務將會被丟棄,然後將該任務加到隊尾

4)ThreadPoolExecutor.AbortPolicy(默認):回絕任務並拋出異常

F、AOP

ThreadPoolExecutor提供了兩個方法在每個任務的執行前後進行調用ThreadPoolExecutor#beforeExecute和ThreadPoolExecutor#afterExecute.

 

4、開頭實例套用

實例中構建的線程池參數:

  • corePoolSize==5    
  • maximumPoolSize==10  
  • keepAliveTime==30s 
  • 隊列:ArrayBlockingQueue,大小爲10
  • 線程工廠:defaultThreadFactory(默認)
  • 回絕策略:AbortPolicy(默認)

套一下工作機理:

1)當併發提交了<=5個任務到executor中時(此時任務數<=corePoolSize),executor會使用5個核心線程去執行這些任務;

2)當這時候馬上又來了一個任務,如果此時5個核心線程有空閒線程的話,就是用空閒的線程去處理,如果都在忙,這時候該任務進入隊列;

3)之後再來任務,還是像第二步那樣去執行,直到任務將隊列放滿了,這時候,如果再來一個任務,如果5個核心線程有空閒線程,直接去執行該任務,如果5個核心線程都在忙,這時候就創建一個新的線程來執行該任務;

4)如果通過上邊的流程,最後5個線程都在忙,並且隊列滿了,並且pool中的線程數已經是10個了(池中的線程總數==maximumPoolSize了),這時候就要執行回絕策略了,在這裏,使用了默認的AbortPolicy,即直接放棄該任務並拋出異常。

在代碼的執行過程中,如果發現後來創建的5個線程有超過30秒都沒被調用過的,該線程就被回收掉了。

 

5、線程池生命週期 

  • 創建之初,狀態爲RUNNNG
  • 調用了ExecutorService#shutdown:將之前已經提交上來的任務進行處理(包括隊列中的),但是不再接收新任務(使用回絕策略回絕新任務),狀態SHUNTDOWN
  • 調用了ExecutorService#shutdownNow:取消所有運行中的任務(包括隊列中的),並且不再接收新任務(使用回絕策略回絕新任務),狀態STOP/TERMINATED

疑問:(這個疑問我會在看完ThreadPoolExecutor的相關源碼後進行回答)

當隊列滿了之後,這時候來了一個任務,恰好5個核心線程有一個空閒了,那麼下面兩種情況哪一個正確:

1)這個空閒的核心線程直接執行剛剛到來的任務

2)這個空閒的核心線程直接執行隊列頭部的任務,而將剛剛到來的任務放入隊尾

解答:這個問題的答案就一句話,有空閒核心線程,就是用核心線程去執行任務;沒有空閒的核心線程,任務纔會入隊。所以選1)


發佈了58 篇原創文章 · 獲贊 32 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章