線程池的概念和定義
在服務器端的業務應用開發中,Web服務器(諸如Tomcat、Jetty)需要接受并處理http請求,所以會為一個請求來分配一個線程來進行處理。如果每次請求都新創建一個線程的話實現起來非常簡便,但是存在這樣的嚴重問題:
隨著業務量的增加,如果并發的請求數量非常多,但每個線程執行的時間很短,這樣就會頻繁的創建和銷毀線程(包括涉及JVM的GC),如此一來會大大降低業務系統的效率。可能出現服務器在為每個請求創建新線程和銷毀線程上花費的時間和消耗的系統資源要比處理實際的用戶請求的時間和資源更多。
那么有沒有一種解決方案可以使線程在執行完一個任務后,不被銷毀,而是可以繼續執行其他的任務呢?
這就是線程池的出現的原因了,其為線程生命周期的開銷和資源不足問題提供了解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。實際應用中,上文所述Tomcat這樣的Web服務器也是利用線程池機制來接收并處理大量并發的http請求,可以通過其server.xml配置文件中的Connect節點的maxThreads(最大線程數)/maxSpareThreads(最大空閑線程數)/minSpareThreads(最小空閑線程數)/acceptCount(最大等待隊列數)/maxIdleTime(最大空閑時間)等參數進行線程池調優。
線程池的定義
線程池是一種多線程任務處理形式,處理過程中將任務添加到隊列,然后在創建線程后自動啟動這些任務。這里引用wiki上面的一個圖如下:
線程池使用的場景
(a)單個任務處理時間相對短
(b)需要處理的任務數量很大
線程池的主要作用
(a)降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
(b)提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
(c)提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
從一個簡單例子說起
在JDK 1.5后引入的Executor線程池框架的最大優點是把任務的提交和執行解耦。要執行任務的人只需把Task描述清楚,然后提交即可。這個Task任務是怎么被執行的,被誰執行的,什么時候執行的,提交的人就不用關心了。在最上面的Executor接口中定義了execute方法,該方法接收Runnable類型的任務命令,對用戶屏蔽底層線程的實現與調度細節,這是一種典型命令設計模式的應用。如下為一段非常簡單的線程池代碼例子:
// ExecutorService fixedThreadPool = ? ? ? ? ? ? ? ?Executors.newFixedThreadPool(5); ? ? ? ?fixedThreadPool.execute(new Runnable() { ? ? ? ? ? ?
????@Override ? ? ? ? ? ?
????????public void run() { ? ? ? ? ? ? ? ?
????????????????logger.info("the task is running") ? ? ? ? ? ?
????} ? ? ? ?});
在上面的例子中,生成線程池采用了工具類Executors的靜態方法,設置了線程池中核心線程數和最大線程數均為5,線程池中超過核心線程數目的空閑線程最大存活時間為0,同時使用LinkedBlockingQueue這樣子的無界阻塞任務隊列。除了newFixedThreadPool可以生成線程數固定大小的線程池,newCachedThreadPool可以生成一個可緩存且自動回收的線程池,newSingleThreadScheduledExecutor可以生成一個單個線程的線程池。newScheduledThreadPool還可以生成支持周期任務的線程池。
ThreadPoolExecutor線程池源碼剖析
JDK中的Executor線程池框架是一個根據一組執行策略調用、調度、執行和控制線程的異步任務框架,其目的是提供一種將“任務提交”與“任務運行”分離開來的機制。作為Java線程池框架中繼承Executor接口最為核心的類—ThreadPoolExecutor,有必要對其源代碼進行深入分析。因此,本節以ThreadPoolExecutor的源代碼舉例,先對以Executor接口為核心的類結構圖進行一個較為全面的展示,然后回歸到源代碼中,對線程池中任務如何提交、如何執行任務等方面分別進行闡述。為了控制篇幅,突出主要邏輯,文章中引用的代碼片段去掉了非重點部分。
J.U.C線程池框架類圖
從上面的類結構圖中可以看出,在JDK的J.U.C包下面主要包含了三個接口,分別是:
(a)Executor:一個運行新任務的簡單接口;
(b)ExecutorService:擴展了Executor接口,增加了一些用來管理線程池狀態和任務生命周期的方法以及支持Future返回值任務提交的方法;
(c)ScheduledExecutorService:擴展了ExecutorService,增加了定期和延遲任務執行的方法;
這三個接口定義了JDK中Executor線程池框架的標準行為,這三個接口的具體代碼可以參考JDK的代碼。限于篇幅,這里就不對各個方法進行一一詳細敘述了。類圖中AbstractExecutorService類是一個抽象類,提供了線程池框架的一些模板方法,具體實現由其子類,ThreadPoolExecutor和ScheduledThreadPoolExecutor分別實現。Executors是個工具類,里面提供了很多靜態方法,根據用戶的需求選擇返回不同的線程池實例。
對于類結構圖中左邊半部分定義了ThreadPoolExecutor核心類的成員變量,包括創建線程的類工廠—ThreadFactory/DefaultThreadFactory,用以描述具體任務執行線程的內部類—Worker(其繼承AQS框架和Runnable接口)和提供線程池工作飽和策略的—RejectedExecutionHandler。
線程池的狀態
線程有五種狀態:新建、就緒、運行、阻塞、死亡。對于線程池來說,同樣也具有五種狀態:RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。線程池狀態轉換圖如下:
在具體說說線程池的五種狀態之前有必要結合ThreadPoolExecutor核心類的代碼進行一些分析,在該類的代碼中對于線程池的五種狀態定義如下:
private final AtomicInteger ctl =new AtomicInteger(ctlOf(RUNNING,0)); ? ?
private static final int COUNT_BITS = Integer.SIZE -3; ? ?
private static final int CAPACITY ? = (1<< COUNT_BITS) -1;// runState is stored in the high-order bits
private static final int RUNNING ? ?= -1<< COUNT_BITS; ? ?
private static final int SHUTDOWN ? =0<< COUNT_BITS; ? ?
private static final int STOP ? ? ? =1<< COUNT_BITS; ? ?
private static final int TIDYING ? ?=2<< COUNT_BITS; ? ?
private static final int TERMINATED =3<< COUNT_BITS;// Packing and unpacking ctl
private static int runStateOf(int c) ? ? {
????return c & ~CAPACITY;
} ? ?
private static int workerCountOf(int c) ?{
? ? return c & CAPACITY;
} ? ?
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
?????? 對于上面代碼中定義的常量ctl是對線程池的運行狀態和線程池中有效線程的數量進行控制的一個32位字段,它包含兩部分的信息:其中高三位表示線程池的運行狀態 (runState) ,低29位表示線程池內有效線程的數量 (workerCount)。COUNT_BITS 就是29,CAPACITY就是1左移29位減1(29個1),這個常量表示workerCount的上限值,大約是5億。另外這里還定義了三個靜態方法分別為,runStateOf—獲取運行狀態;workerCountOf—獲取活動線程數;ctlOf—獲取運行狀態和活動線程數的值。
(a)RUNNING:處于RUNNING狀態的線程池能夠接受新任務,以及對新添加的任務進行處理。
(b)SHUTDOWN:處于SHUTDOWN狀態的線程池不可以接受新任務,但是可以對已添加的任務進行處理。
(c)STOP:處于STOP狀態的線程池不接收新任務,不處理已添加的任務,并且會中斷正在處理的任務。
(d)TIDYING:當所有的任務已終止,ctl記錄的"任務數量"為0,線程池會變為TIDYING狀態。當線程池變為TIDYING狀態時,會執行鉤子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變為TIDYING時,進行相應的處理;可以通過重載terminated()函數來實現。
(e)TERMINATED:線程池徹底終止的狀態。
創建Executor線程池方法
在上文第二節內容中,已經給出了一個創建簡單線程池的例子,其中調用了JDK的ThreadPoolExecutor核心類的構造函數來創建的線程池實例。在這一節內容中,通過分析ThreadPoolExecutor核心類的構造函數以及參數來看下如何創建一個Executor線程池以及在創建時候需要關注哪些要素?
(a)corePoolSize:該參數指的是線程池中核心線程的數量。當提交一個任務時,線程池會新建一個線程來執行任務,直到當前線程數等于corePoolSize。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建并啟動所有基本線程
(b)maximumPoolSize:從參數名稱上也應該可以明白它的意思。該參數指的是線程池中允許的最大線程數。線程池的阻塞隊列滿了之后,如果還有任務提交,如果當前的線程數小于maximumPoolSize,則會新建線程來執行任務。這里有必要說明的是,如果使用的是無界隊列,該參數也就沒有什么效果了。
(c)keepAliveTime:該參數為線程空閑的時間。線程的創建和銷毀是需要代價的。線程執行完任務后不會立即銷毀,而是繼續存活一段時間:keepAliveTime。默認情況下,該參數只有在線程數大于corePoolSize時才會生效。
(d)unit:該參數用于表示keepAliveTime的單位。
(e)workQueue:該參數用來表示保存線程池中等待執行任務的阻塞隊列,等待的任務需要實現Runnable接口。我們可以選擇這幾種:ArrayBlockingQueue:基于數組結構的有界阻塞隊列,FIFO;LinkedBlockingQueue:基于鏈表結構的無界阻塞隊列(如果設置初始化時的隊列大小),FIFO;SynchronousQueue:不存儲元素的阻塞隊列,每個插入操作都必須等待一個移出操作,反之亦然;PriorityBlockingQueue:具有優先級別的阻塞隊列。這里的幾種阻塞隊列都為JDK中J.U.C并發包下較為經典的阻塞隊列,其源碼值得閱讀和學習,有興趣的朋友可以自己閱讀。
(f)threadFactory:該參數用于設置線程池中創建工作線程的工廠對象。該對象可以通過Executors.defaultThreadFactory()返回。
(g)handler:該參數為線程池的拒絕策略。所謂拒絕策略,是指將任務添加到線程池中時,線程池拒絕該任務所采取的相應策略。當向線程池中提交任務時,如果此時線程池中的線程已經飽和了,而且阻塞隊列也已經滿了,則線程池會選擇一種拒絕策略來處理該任務。線程池提供的拒絕策略主要有以下四種(位于上面J.U.C線程池的類結構圖中左半部分):
AbortPolicy:直接拋出異常,默認的線程池拒絕策略;
CallerRunsPolicy:用調用者所在的線程來完成待執行的任務;
DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,并執行當前任務;
DiscardPolicy:直接丟棄任務;
下面這個圖為線程池的邏輯結構圖:
由上面的邏輯結構圖可以初步知道線程池主要的執行流程:
(a)當有任務進入時,線程池創建線程去執行任務,直到核心線程數滿為止;
(b)核心線程數量滿了之后,任務就會進入一個緩沖的任務隊列中;
1、當任務隊列為無界隊列時(諸如LinkedBlockingQueue隊列,通過構造函數來配置為無界隊列),任務就會一直放入緩沖的任務隊列中,不會和最大線程數量進行比較。
2、當任務隊列為有界隊列時(諸如ArrayBlockingQueue隊列),任務先放入緩沖的任務隊列中,當任務隊列滿了之后,才會將任務放入線程池,此時會與線程池中最大的線程數量進行比較,如果超出了,則默認會拋出異常進行拒絕動作。否則,線程池才會執行任務。當任務執行完,又會將緩沖隊列中的任務推入線程池中,然后重復此操作。
以下是線程池采用有界隊列來處理任務的主要流程如下圖所示:
線程池中任務提交與執行
本節將介紹線程池中最為核心的任務提交代碼流程。Executor線程池可以根據業務需求的不同提供兩種方式提交任務:Executor.execute()、ExecutorService.submit()。在實際應用中,我們可以使用execute方法來提交沒有返回值的任務。因為沒有返回值,所以實際也沒有辦法知道任務是否被線程池中的線程執行成功。通過以下的示例代碼可以提交一個沒有返回值的任務:
threadpool.execute(newRunnable() {
@Override
public void run() {
//TODO
//logical code here
}});
另外,我們也可以通過submit方法來提交帶有返回類型future的任務,通過這個返回值可以判斷任務是否已經成功執行。通過future的get方法可以獲取任務執行的返回值,這里需要說明的是get方法是阻塞的,直到任務返回為止,也可以通過get(long timeout,TimeUnit unit方法)設置超時時間,避免一直阻塞等待。以下為submit方法提交任務的示例代碼:
Future future = executor.submit();
try{
Object ret = future.get();
}catch(InterruptedException e1){
//處理中斷異常
}catch(ExecutionException e2){
//處理無法執行任務異常
}finally{
//最后關閉線程池executor.shutdown();
}
限于篇幅,本文僅對ThreadPoolExecutor核心類的execute方法的實現進行深入分析,而對submit方法則不做分析闡述,感興趣的朋友可以自行按照同樣的方法進行分析(其實,通過看submit的源代碼可以發現,它實際上還是調用的execute方法,只不過它利用了Future來獲取任務執行結果)。execute方法的具體代碼如下:
從以上的ThreadPoolExecutor類的execute方法的實現代碼中可以得出以下幾步主要的執行流程:
Step1:如果線程池當前線程數小于corePoolSize,則調用addWorker創建新線程執行任務,且方法返回,如果調用失敗執行Step2;
Step2:如果線程池處于RUNNING狀態,則嘗試加入待執行任務的阻塞隊列;如果加入該隊列成功,則嘗試進行Double Check;如果加入失敗,則執行Step3;
Step3:如果線程池當前為非RUNNING狀態或者加入阻塞隊列失敗,則嘗試創建新線程直到maxPoolSize;如果失敗,則調用reject()方法執行相應的飽和拒絕策略;
這里需要注意的是,在Step2中如果加入隊列成功,則會進行一個雙重校驗的過程。其主要目的是判斷加入到阻塞隊列中的任務是否可以被執行。如果線程池不是RUNNING狀態,則調用remove()方法從阻塞隊列中刪除該任務,然后調用reject()方法處理任務。否則需要確保還有線程執行。
在上面的executor方法中多次調用了addWorker方法,可能已經有同學在默默關注這個方法了。addWorker方法的主要工作是在線程池中創建一個新的線程并執行,firstTask參數 用于指定新增的線程執行的第一個任務,core參數為true表示在新增線程時會判斷當前活動線程數是否少于corePoolSize,false表示新增線程前需要判斷當前活動線程數是否少于maximumPoolSize。下面我們主要來看下這個addWork方法里面的具體實現代碼:
private boolean addWorker(Runnable firstTask, boolean core) { ? ? ? ?
retry: for(;;) { ? ? ? ? ? ?
int c = ctl.get();// 獲取當前線程池的狀態
int rs = runStateOf(c);
if(rs >= SHUTDOWN &&! (rs == SHUTDOWN && firstTask ==null&&! workQueue.isEmpty()))
return false;// 內層循環,worker + 1
for(;;) {
// 線程數量
int wc = workerCountOf(c);
// 如果當前線程數大于線程最大上限CAPACITY ?return false// 若core == true,則與corePoolSize 比較,否則與maximumPoolSize ,大于 return false
if(wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;// worker + 1,成功跳出retry循環
if(compareAndIncrementWorkerCount(c)) break retry; ? ? ? ? ? ? ? ?
c = ctl.get();// Re-read ctl
// 如果狀態不等于之前獲取的state,跳出內層循環,繼續去外層循環判斷
if(runStateOf(c) != rs) continue retry; ? ? ? ? ? ?
} ? ? ?
?} ? ? ? ?
boolean workerStarted =false; ? ? ?
?boolean workerAdded =false; ? ? ? ?
Worker w =null;
try{
// 新建線程:Worker
final ReentrantLock mainLock =this.mainLock; ? ? ? ? ? ?
w =new Worker(firstTask); ? ? ? ? ? ?
final Thread t = w.thread;
if(t !=null) { ? ? ? ? ? ? ? ?
mainLock.lock();
try{ ? ? ? ? ? ? ? ? ? ?
int c = ctl.get(); ? ? ? ? ? ? ? ? ? ?
int rs = runStateOf(c);
// rs < SHUTDOWN ==> 線程處于RUNNING狀態// 或者線程處于SHUTDOWN狀態,且firstTask == null(可能是workQueue中仍有未執行完成的任務,創建沒有初始任務的worker線程執行)
if(rs < SHUTDOWN || (rs == SHUTDOWN && firstTask ==null)) {
// 當前線程已經啟動,拋出異常
if(t.isAlive())// precheck that t is startable
throw new IllegalThreadStateException(); ? ? ? ? ? ? ? ? ? ? ? ?
workers.add(w);// 設置最大的池大小largestPoolSize,workerAdded設置為true
int s = workers.size();
if(s > largestPoolSize)?
largestPoolSize = s; ? ? ? ? ? ? ? ? ? ? ? ?
workerAdded =true; ? ? ? ? ? ? ? ? ? ?
} ? ? ? ? ? ? ? ?
}finally{ ? ? ? ? ? ? ? ? ? ?
mainLock.unlock(); ? ? ? ? ? ? ? ?
}
// 啟動線程
if(workerAdded) { ? ? ? ? ? ? ? ? ?
?t.start(); ? ? ? ? ? ? ? ? ?
?workerStarted =true; ? ? ? ? ? ? ? ?
} ? ? ? ? ? ?
} ? ? ? ?
}finally{
if(! workerStarted) ? ? ? ? ? ? ? ?
addWorkerFailed(w); ? ? ? ?
}
return workerStarted; ? ?
}
如果對Executor線程池本身就有所了解的同學可能可以以上面的代碼加上注釋就可以明白addWorker方法中的具體含義了。但是這里仍然有必要再說下,在上面的addWorker方法的代碼中,主要完成了以下幾步流程:
Step1.首先,獲取線程池的狀態后先進行條件的判斷,如果rs >= SHUTDOWN,則表示此時不再接收新任務;其次,判斷以下三個條件只要有一個不滿足則addWorker方法返回false。這三個條件分別為:
(a)rs == SHUTDOWN,此時表示線程池處于關閉狀態,不再接受新提交的任務,但卻可以繼續處理阻塞隊列中已保存的任務;
(b)firsTask為空;
(c)阻塞隊列不為空;
這里,首先考慮rs == SHUTDOWN的情況,在這種情況下不會接受新提交的任務,所以在firstTask不為空的時候會返回false;其次,如果firstTask為空,并且workQueue也為空,則返回false,因為隊列中已經沒有任務了,不需要再添加線程了。
Step2.這里先獲取線程數,根據addWorker方法的第二個參數為true或者false進行判斷。如果為true表示根據corePoolSize來比較,如果為false則根據maximumPoolSize來比較。然后,通過CAS進行worker + 1。
Step3.獲取主鎖mailLock,隨后再次判斷線程池的狀態。如果線程池處于RUNNING狀態或者是處于SHUTDOWN狀態且 firstTask == null,則向線程池中添加線程,然后釋放主鎖mainLock并啟動線程,最后return true。如果中途失敗導致workerStarted= false,則調用addWorkerFailed()方法進行處理。這里需要注意的是,t.start()這個語句,啟動時會調用Worker類中的run方法,Worker本身實現了Runnable接口,所以一個Worker類型的對象也是一個線程。
仔細的同學會發現上面addWorker方法的代碼中還有一個Worker對象,在線程池中每一個線程被封裝成一個Worker對象,線程池中維護的其實就是一組Worker對象,那么我們來看一下Worker的定義:
private final class Worker? ? ?extends AbstractQueuedSynchronizer ? ? ? ?implements Runnable{
//省略代碼......
/** Thread this worker is running in. ?Null if factory fails. */
final Thread thread;
/** Initial task to run. ?Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
? ? * Creates with given first task and thread from ThreadFactory.
? ? * @param firstTask the first task (null if none)
? ? */
Worker(Runnable firstTask) { ? ? ? ?
setState(-1);// inhibit interrupts until run Worker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); ? ?
}
/** Delegates main run loop to outer runWorker ?*/
public void run() { ? ? ? ?
runWorker(this); ? ?
}
//省略代碼......
protected boolean tryAcquire(int unused) {
if(compareAndSetState(0,1)) { ? ? ? ? ? ?
setExclusiveOwnerThread(Thread.currentThread());
return true; ? ? ? ?
}
return false; ? ?
} ? ?
protected boolean tryRelease(int unused) { ? ? ? ?
setExclusiveOwnerThread(null); ? ? ? ?
setState(0);
return true; ? ?
} ? ?
public void lock() ? ? ? ?{
acquire(1);
} ? ?
public boolean tryLock() ?{
returntryAcquire(1);
} ? ?
public void unlock() ? ? ?{
release(1);
} ? ?
public boolean isLocked() {
return isHeldExclusively();
}
//省略代碼......
}
內部Worker類繼承了AQS,并實現了Runnable接口,其中firstTask用它來保存傳入的任務;thread是在調用構造方法時通過ThreadFactory來創建的線程,是用來處理任務的線程。在調用構造方法時,需要把任務傳入,這里通過getThreadFactory().newThread(this);來新建一個線程,newThread方法傳入的參數是this,因為Worker本身繼承了Runnable接口,也就是一個線程,所以一個Worker對象在啟動的時候會調用Worker類中的run方法。同時,該類繼承了AQS框架,使用其來實現獨占鎖的功能。這里可能有同學會問為什么不使用ReentrantLock來實現呢?可以看到tryAcquire方法,它是不允許重入的,而ReentrantLock是允許重入的。這里,之所以設置為不可重入,是因為不希望任務在調用類似像setCorePoolSize這樣的線程池控制方法時重新獲取鎖,而去中斷正在運行的線程。
講到這里還沒有看到線程池任務運行的代碼,對Java線程Runnable接口比較熟悉的同學可能知道應該是在上面的run方法來執行具體的任務運行,那么下面我們再進一步的看下runWorker方法里面究竟是干什么的?
這里總結一下runWorker方法的執行過程:
Step1.外層循環不斷地通過getTask()方法獲取任務,其中getTask()方法從阻塞隊列中取任務;
Step2. 如果線程池當前正在停止,那么要保證當前線程是中斷狀態,否則要保證當前線程不是中斷狀態;
Step3.調用task.run()去執行任務,這里就是真正去執行任務了;
Step4.如果task為null則跳出循環,執行processWorkerExit()方法;
上面代碼中的getTask()方法是用于從阻塞隊列中獲取任務的;processWorkerExit()方法則進行收尾工作,統計完成的任務數目并從線程池中移除一個工作線程以銷毀工作線程。講到這里基本可以總結下整個工作線程的生命周期:從execute方法開始,Worker使用ThreadFactory創建新的工作線程,runWorker通過getTask獲取任務,然后執行任務,如果getTask返回null,進入processWorkerExit方法,整個線程結束。執行流程圖如下:
限于篇幅所限,對于ThreadPoolExecutor核心類中的tryTerminate、shutdown、shutdownNow和interruptIdleWorkers等方法就不再在此進行贅述了。感興趣的同學可以自己再閱讀這幾個類的源代碼。
如何合理應用線程池
一般在我們自己的Spring-boot工程中都用如下的配置方式來注入線程池的Bean(Spring-MVC的工程是基于XML,配置大同小異)。不注意其中細節的同學覺得這里的幾個參數按照自己的感覺隨便設置即可,如果是自己做練習或者一些demo樣例的話,確實是無所謂,但是要應用于生產實際環境的話,還是需要經過一番分析和思考來設置線程池的參數,才能滿足業務需求達到優化系統性能的目標。
@Bean(name ="taskAsyncPool")
public Executor taskAsyncPool() { ? ? ? ? ?
ThreadPoolTaskExecutor executor =new ThreadPoolTaskExecutor(); ? ? ? ? ? executor.setCorePoolSize(corePoolSize); ? ? ? ? ?
executor.setMaxPoolSize(maxPoolSize); ? ? ? ? ?
executor.setQueueCapacity(queueCapacity); ? ? ? ? ? executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut); ? ? ? ? ? executor.setKeepAliveSeconds(keepAliveSeconds); ? ? ? ? ? executor.setThreadNamePrefix("ExecutorThread-");// rejection-policy:當pool已經達到max size的時候,如何處理新任務// CALLER_RUNS:不在新線程中執行任務,而是有調用者所在的線程來執行executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy()); ? ? ? ? ? executor.initialize();
return executor;
}
要合理地應用線程池技術,就需要首先對任務的特性有足夠的了解,可以從以下的幾個方面來分析:
(a)任務性質:提交的任務到底是CPU密集型,IO密集型還是兼有兩者的混合型;
(b)任務優先級:任務是否具備低、中、高的優先級;
(c)任務執行時間:任務執行時間需要較長、中、還是較短;
(d)任務依賴性:是否依賴其他系統,比如數據庫連接;
首先,對于(a)我們可以根據任務性質的不同用不同規模的線程池來進行處理。CPU密集型的任務可以盡可能配置核心線程數較小的線程池,如配置Ncpu+1個線程的線程池。而對于IO密集型的任務由于任務長時間處于IO等待中,因此可以配置較多的線程,如2*Ncpu線程的線程池。如果對于混合型則通過適當地將其拆分成CPU密集型和IO密集型的分別處理。如果編程者不確定當前機器的CPU核數,JDK提供了Runtime.getRuntime().availableProcessors()方法進行獲取。
對于(b)可以通過J.U.C并發包中的優先級隊列—PriorityBlockingQueue來進行任務處理。它可以讓高優先級的任務先執行,低優先級任務延遲執行。
對于(c)(d)可以根據任務不同執行時間,分別建立不同規模類型的線程池來進行任務處理。
另外,需要說明的是在實際應用中,還是建議在線程池中設置有界隊列來初始化。因為有界隊列可以增加系統的穩定性和預警設置,如果遇到線程池中線程數和任務隊列均滿的情況,可以直接執行飽和拒絕策略并拋出異常告警。若是采用了無界隊列,則線程池中的任務隊列的任務數目會積壓得越來越多,最后撐爆服務器的內存,造成整個系統不可用。
總結
本文從線程池的概念和定義出發,簡單介紹了其使用場景和主要作用。然后從一個簡單的線程池樣例說起,闡述了Executor線程池框架的任務的提交和執行解耦機制。通過給出J.U.C包下與線程池相關的類結構圖,簡要介紹了以ThreadPoolExecutor為核心的相關接口和類,并結合代碼給出線程池幾種狀態定義以及轉換圖,詳細描述了創建線程池方法的ThreadPoolExecutor構造函數。最后,以ThreadPoolExecutor類的execute方法為入口深入分析addWorker和runWorker核心方法的源代碼,梳理了線程池整體工作原理、生命周期和運行機制。在向線程池提交任務時,除本文敘述的execute方法外,還有一個submit方法,submit方法會返回一個Future對象用于獲取返回值,限于篇幅,有關Future和Callable將在其他篇幅中進行介紹。限于筆者的才疏學淺,對JDK的Executor線程池可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。