你很了解線程池?可沒你想的那么簡單!

前言

原以為線程池還挺簡單的(平時常用,也分析過原理),這次是想自己動手寫一個線程池來更加深入的了解它;但在動手寫的過程中落地到細節(jié)時發(fā)現并沒想的那么容易。結合源碼對比后確實不得不佩服 Doug Lea 。

我覺得大部分人直接去看 java.util.concurrent.ThreadPoolExecutor 的源碼時都是看一個大概,因為其中涉及到了許多細節(jié)處理,還有部分 AQS 的內容,所以想要理清楚具體細節(jié)并不是那么容易。

與其挨個分析源碼不如自己實現一個簡版,當然簡版并不意味著功能缺失,需要保證核心邏輯一致。

所以也是本篇文章的目的:自己動手寫一個五臟俱全的線程池,同時會了解到線程池的工作原理,以及如何在工作中合理的利用線程池。

再開始之前建議對線程池不是很熟悉的朋友看看有關線程池的這兩篇文章:這里我截取了部分內容,也許可以埋個伏筆(坑)。

image

具體請看這兩個鏈接:

(1)如何優(yōu)雅的使用和理解線程池

(2)線程池中你不容錯過的一些細節(jié)

一、創(chuàng)建線程池

現在進入正題,新建了一個 CustomThreadPool 類,它的工作原理如下:

image

簡單來說就是往線程池里邊丟任務,丟的任務會緩沖到隊列里;線程池里存儲的其實就是一個個的 Thread ,他們會一直不停的從剛才緩沖的隊列里獲取任務執(zhí)行。

流程還是挺簡單。

先來看看我們這個自創(chuàng)的線程池的效果如何吧:

image
image

初始化了一個核心為3、最大線程數為5、隊列大小為 4 的線程池。

先往其中丟了 10 個任務,由于阻塞隊列的大小為 4 ,最大線程數為 5 ,所以由于隊列里緩沖不了最終會創(chuàng)建 5 個線程(上限)。

過段時間沒有任務提交后(sleep)則會自動縮容到三個線程(保證不會小于核心線程數)。

1、構造函數

來看看具體是如何實現的。

下面則是這個線程池的構造函數:

image

會有以下幾個核心參數:

image

大致上都和 ThreadPool 中的參數相同,并且作用也是類似的。

需要注意的是其中初始化了一個 workers 成員變量:

/** * 存放線程池 */

privatevolatileSet workers;

public CustomThreadPool(int miniSize,int maxSize,longkeepAliveTime, TimeUnit unit, BlockingQueue workQueue, Notify notify){              

 workers =newConcurrentHashSet<>();    

}

workers 是最終存放線程池中運行的線程,在 j.u.c 源碼中是一個 HashSet 所以對他所有的操作都是需要加鎖。

我這里為了簡便起見就自己定義了一個線程安全的 Set 稱為 ConcurrentHashSet。

image

其實原理也非常簡單,和 HashSet 類似也是借助于 HashMap 來存放數據,利用其 key 不可重復的特性來實現 set ,只是這里的 HashMap 是用并發(fā)安全的 ConcurrentHashMap 來實現的。

這樣就能保證對它的寫入、刪除都是線程安全的。

不過由于 ConcurrentHashMap 的 size() 函數并不準確,所以我這里單獨利用了一個 AtomicInteger 來統(tǒng)計容器大小。

2、創(chuàng)建核心線程

往線程池中丟一個任務的時候其實要做的事情還蠻多的,最重要的事情莫過于創(chuàng)建線程存放到線程池中了。

當然我們不能無限制的創(chuàng)建線程,不然拿線程池來就沒任何意義了。于是 miniSize maxSize 這兩個參數就有了它的意義。

但這兩個參數再哪一步的時候才起到作用呢?這就是首先需要明確的。

image

結合代碼可以發(fā)現在執(zhí)行任務的時候會判斷是否大于核心線程數,從而創(chuàng)建線程。

worker.startTask() 執(zhí)行任務部分放到后面分析。

這里的 miniSize 由于會在多線程場景下使用,所以也用 volatile 關鍵字來保證可見性。

3、隊列緩沖

結合上面的流程圖,第二步自然是要判斷隊列是否可以存放任務(是否已滿)。

4、上至封頂

一旦寫入失敗則會判斷當前線程池的大小是否大于最大線程數,如果沒有則繼續(xù)創(chuàng)建線程執(zhí)行。

不然則執(zhí)行會嘗試阻塞寫入隊列(j.u.c 會在這里執(zhí)行拒絕策略)

以上的步驟和剛才那張流程圖是一樣的,這樣大家是否有看出什么坑嘛?

5、時刻小心

從上面流程圖的這兩步可以看出會直接創(chuàng)建新的線程

這個過程相對于中間直接寫入阻塞隊列的開銷是非常大的,主要有以下兩個原因:

創(chuàng)建線程會加鎖,雖說最終用的是 ConcurrentHashMap 的寫入函數,但依然存在加鎖的可能。

會創(chuàng)建新的線程,創(chuàng)建線程還需要調用操作系統(tǒng)的 API 開銷較大。

所以理想情況下我們應該避免這兩步,盡量讓丟入線程池中的任務進入阻塞隊列中。

二、執(zhí)行任務

任務是添加進來了,那是如何執(zhí)行的?

在創(chuàng)建任務的時候提到過 worker.startTask() 函數:

/**    * 添加任務,需要加鎖    *@paramrunnable 任務    */

privatevoidaddWorker(Runnable runnable){        

Worker worker =newWorker(runnable,true);        

worker.startTask();        

workers.add(worker);    

}

也就是在創(chuàng)建線程執(zhí)行任務的時候會創(chuàng)建 Worker 對象,利用它的 startTask() 方法來執(zhí)行任務。

所以先來看看 Worker 對象是長啥樣的:

image

其實他本身也是一個線程,將接收到需要執(zhí)行的任務存放到成員變量 task 處。

而其中最為關鍵的則是執(zhí)行任務 worker.startTask() 這一步驟。

publicvoidstartTask(){        

thread.start();    

}

其實就是運行了 worker 線程自己,下面來看 run 方法。

第一步是將創(chuàng)建線程時傳過來的任務執(zhí)行(task.run),接著會一直不停的從隊列里獲取任務執(zhí)行,直到獲取不到新任務了。

任務執(zhí)行完畢后將內置的計數器 -1 ,方便后面任務全部執(zhí)行完畢進行通知。

worker 線程獲取不到任務后退出,需要將自己從線程池中釋放掉(workers.remove(this))。

1、從隊列里獲取任務

其實 getTask 也是非常關鍵的一個方法,它封裝了從隊列中獲取任務,同時對不需要保活的線程進行回收。

image

很明顯,核心作用就是從隊列里獲取任務;但有兩個地方需要注意:

(1)當線程數超過核心線程數時,在獲取任務的時候需要通過保活時間從隊列里獲取任務;一旦獲取不到任務則隊列肯定是空的,這樣返回 null 之后在上文的 run() 中就會退出這個線程;從而達到了回收線程的目的,也就是我們之前演示的效果

image

(2)這里需要加鎖,加鎖的原因是這里肯定會出現并發(fā)情況,不加鎖會導致 workers.size() > miniSize 條件多次執(zhí)行,從而導致線程被全部回收完畢。

三、關閉線程池

最后來談談線程關閉的事;

image

還是以剛才那段測試代碼為例,如果提交任務后我們沒有關閉線程,會發(fā)現即便是任務執(zhí)行完畢后程序也不會退出。

從剛才的源碼里其實也很容易看出來,不退出的原因是 Worker 線程一定還會一直阻塞在 task = workQueue.take(); 處,即便是線程縮容了也不會小于核心線程數。

通過堆棧也能證明:

image

恰好剩下三個線程阻塞于此處。

而關閉線程通常又有以下兩種:

(1)立即關閉:執(zhí)行關閉方法后不管現在線程池的運行狀況,直接一刀切全部停掉,這樣會導致任務丟失。

不接受新的任務,同時等待現有任務執(zhí)行完畢后退出線程池。

我們先來看第一種立即關閉:

/*** 立即關閉線程池,會造成任務丟失*/

publicvoidshutDownNow(){        isShutDown.set(true);        

tryClose(false);    

}

/**    * 關閉線程池    *    *@paramisTry true 嘗試關閉      --> 會等待所有任務執(zhí)行完畢    *              false 立即關閉線程池--> 任務有丟失的可能   */

privatevoidtryClose(booleanisTry){

if(!isTry) {            

closeAllTask();       

 }else{

if(isShutDown.get() && totalTask.get() ==0) {                

closeAllTask();            

}       

 }   

 }

/* * 關閉所有任務*/

private void closeAllTask(){ 

for(Worker worker : workers) {

//LOGGER.info("開始關閉");

worker.close();        

}    

}

public void close(){        

thread.interrupt();   

 }

很容易看出,最終就是遍歷線程池里所有的 worker 線程挨個執(zhí)行他們的中斷函數。

我們來測試一下:

可以發(fā)現后面丟進去的三個任務其實是沒有被執(zhí)行的。

(2)完事后關閉

正常關閉則不一樣:

/*** 任務執(zhí)行完畢后關閉線程池*/

public void shutdown(){        

isShutDown.set(true);       

 tryClose(true);    

}

他會在這里多了一個判斷,需要所有任務都執(zhí)行完畢之后才會去中斷線程。

同時在線程需要回收時都會嘗試關閉線程:

來看看實際效果:

四、回收線程

上文或多或少提到了線程回收的事情,其實總結就是以下兩點:

image

一旦執(zhí)行了 shutdown/shutdownNow 方法都會將線程池的狀態(tài)置為關閉狀態(tài),這樣只要 worker 線程嘗試從隊列里獲取任務時就會直接返回空,導致 worker 線程被回收。

一旦線程池大小超過了核心線程數就會使用保活時間來從隊列里獲取任務,所以一旦獲取不到返回 null 時就會觸發(fā)回收。

但如果我們的隊列足夠大,導致線程數都不會超過核心線程數,這樣是不會觸發(fā)回收的。

比如這里我將隊列大小調為 10 ,這樣任務就會累計在隊列里,不會創(chuàng)建五個 worker 線程。

所以一直都是 Thread-1~3 這三個線程在反復調度任務。

五、總結

本次實現了線程池里大部分核心功能,我相信只要看完并動手敲一遍一定會對線程池有不一樣的理解。

結合目前的內容來總結下:

線程池、隊列大小要設計的合理,盡量的讓任務從隊列中獲取執(zhí)行。

慎用 shutdownNow() 方法關閉線程池,會導致任務丟失(除非業(yè)務允許)。

如果任務多,線程執(zhí)行時間短可以調大 keepalive 值,使得線程盡量不被回收從而可以復用線程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380