這是張小帥失業之后的第三場面試。
面試官:“實際開發中用過多線程吧,那聊聊線程池吧”。
“有CachedThreadPool:可緩存線程池,FixedThreadPool:定長線程池.......balabala”。小帥暗暗竊喜,還好把這幾種線程池背下來了,看來這次可以上岸了。
面試官點點頭,繼續問到“那線程池底層是如何實現復用的?”
“額,這個....”
寒風中,那個男人的背影在暮色中顯得孤寂而凄涼,仿佛與世隔絕,獨自面對著無盡的寂寞......
如果問到線程池的話,不好好剖析過底層代碼,恐怕真的會像小帥那樣被問翻吧。
那么在此我們就來好好剖析一下線程池的底層吧。我們大概從如下幾個方面著手:
概覽圖
說到線程池,其實我們要先聊到池化技術。
池化技術:我們將資源或者任務放入池子,使用時從池中取,用完之后交給池子管理。通過優化資源分配的效率,達到性能的調優。
池化技術優點:
所以我們說線程池是提升線程可重復利用率、可控性的池化技術的一種。
現在我們有這樣一個場景,上層有業務系統批量調用底層進行發送郵件,廢話不多,直接上代碼:
demo
最終運行輸出結果為:
由線程:pool-1-thread-1 發送第:0封郵件由線程:pool-1-thread-2 發送第:1封郵件由線程:pool-1-thread-1 發送第:2封郵件由線程:pool-1-thread-2 發送第:3封郵件由線程:pool-1-thread-1 發送第:4封郵件由線程:pool-1-thread-1 發送第:6封郵件由線程:pool-1-thread-2 發送第:5封郵件由線程:pool-1-thread-1 發送第:7封郵件由線程:pool-1-thread-2 發送第:8封郵件由線程:pool-1-thread-1 發送第:9封郵件
上面的例子中從結果來看是10封郵件分別由兩條線程發送出去了,上圖可見,我們給ThreadPoolExecutor這個執行器分別指定了七個參數。那么參數的含義到底是什么呢?接下來咱們層層抽絲剝繭。
大家估計會有疑問,線程池的種類那么多,案例中為什么要用TheadPoolExecutor類呢,其他的種類是由TheadPoolExecutor通過不同的入參定義出來的,所以我們直接拿ThreadPoolExecutor來看。
我們先來看一下ThreadPoolExecutor的繼承關系,有個宏觀印象:
宏觀繼承
我們再來看一下ThreadPoolExecutor的構造方法:
構造方法
下面我們來解釋一下幾個參數的含義:
大家對上述的含義初步有個概念。
看了上面的構造函數字段大家估計也還是優點懵的,尤其是從來沒有接觸過商品池的小伙伴。所以老貓又擼了一張商品池的大概的工作流程圖,方便大家把這些概念串起來。
大概流程
上圖中老貓標記了四條線,簡單介紹一下(當然上圖若有問題,也希望大家能夠指出來)。
接下來我們來看一下執行theadPoolExecutor.execute()的時候到底發生了什么。先來看一下源碼:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
(1) ctl變量
進入執行源碼之后我們首先看到的是ctl,只知道ctl中拿到了一個int數據至于這個數值有什么用,目前不知道,接著看涉及的相關代碼,老貓將相關的代碼解讀放到源碼中進行注釋。
//通過ctl獲取線程池的狀態以及包含的線程數量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // COUNT_BITS = 32-3 = 29 /**001左移29位 * 00100000 00000000 00000000 00000000 * 操作減1 * 00011111 11111111 11111111 11111111(表示初始化的時候線程情況,1表示均有空閑線程) * 換成十進制:COUNT_MASK = 536870911 */ private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; /** * 運行中狀態 * 1的原碼 * 00000000 00000000 00000000 00000001 * 取反+1 * 11111111 11111111 11111111 11111111 * 左移29位 * 11100000 00000000 00000000 00000000 **/ // runState is stored in the high-order bits private static final int RUNNING = -1 << COUNT_BITS; //運行中狀態 11100000 00000000 00000000 00000000 private static final int SHUTDOWN = 0 << COUNT_BITS; //終止狀態 00000000 00000000 00000000 00000000 private static final int STOP = 1 << COUNT_BITS; //停止 00100000 00000000 00000000 00000000 private static final int TIDYING = 2 << COUNT_BITS; // 01000000 00000000 00000000 00000000 private static final int TERMINATED = 3 << COUNT_BITS; // 01100000 00000000 00000000 00000000 //取高3位表示獲取運行狀態 private static int runStateOf(int c) { return c & ~COUNT_MASK; } //~COUNT_MASK表示取反碼:11100000 00000000 00000000 00000000 //取出低位29位的值,當前活躍的線程數 private static int workerCountOf(int c) { return c & COUNT_MASK; } //COUNT_MASK:00011111 11111111 11111111 11111111 //計算ctl的值,ctl=[3位]線程池狀態 + [29位]線程池中線程數量。 private static int ctlOf(int rs, int wc) { return rs | wc; } //進行或運算
上面我們針對各個狀態以及那么多的二進制表示符有點懵,當然如果不會二進制運算的,大家可以先自己去了解一下二進制的運算邏輯。通過源碼中的英文,我們知道CTL的值其實分成兩部分組成,高三位是狀態,其余均為當前線程數。如下的圖:
線程池狀態
上面的圖的描述解釋,其實也都是英文注釋版的翻譯,我們再來看一下有了這些狀態,這些狀態是怎么流轉的,英文注釋是這樣的:
/*** RUNNING -> SHUTDOWN * On invocation of shutdown() * (RUNNING or SHUTDOWN) -> STOP * On invocation of shutdownNow() * SHUTDOWN -> TIDYING * When both queue and pool are empty * STOP -> TIDYING * When pool is empty * TIDYING -> TERMINATED * When the terminated() hook method has completed * /
上面的描述不太直觀,老貓將流程串了起來,得到了下面的狀態機流轉圖。如下圖:
狀態機流程
寫到這里,其實ctl已經很清楚了,ctl說白了就是狀態位和活躍線程數的表示方式。通過ctl咱們可以知道當前是什么狀態以及活躍線程數量是多少 (設計很巧妙,如果此處還有問題,歡迎大家私聊老貓)。
(3) 線程池中的線程數小于核心線程數
讀完ctl之后,我們來看一下接下來的代碼。
if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; //添加新的線程 c = ctl.get(); //重新獲取當前的狀態以及線程數量}
繼上述的workerCountOf,我們知道這個方法可以獲取當前活躍的線程數。如果當前線程數小于配置的核心線程數,則會調用addWorker進行添加新的線程。如果添加失敗了,則重新獲取ctl的值。
(4) 任務添加到隊列的相關邏輯
if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); //再次check一下,當前線程池是否是運行狀態,如果不是運行時狀態,則把剛剛添加到workQueue中的command移除掉 if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); }
上述我們知道當添加線程池失敗的時候,我們會重新獲取ctl的值。此時咱們的第一步就很清楚了:
(5) 線程池中的線程數量小于最大線程數代碼邏輯以及拒絕策略的代碼邏輯
接下來,我們看一下最后的一個步驟
/** * 進入第三步驟前提: * 1.線程池不是運行狀態,所以isRunning(c)為false * 2.workCount >= corePoolSize的時候于此同時并且添加到queue失敗的時候執行 */else if (!addWorker(command, false)) reject(command); }
由于調用addWorker的第二個參數是false,則表示對比的是最大線程數,那么如果往線程池中創建線程依然失敗,即addWorker返回false,那么則進入if語句中,直接調用reject方法調用拒絕策略了。
寫到這里大家估計會對這個第二個參數是false為什么比較的是最大線程數有疑問。其實這個是addWorker中的方法。我們可以大概看一下:
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (int c = ctl.get();;) { // Check if queue empty only if necessary. if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty())) return false; for (;;) { if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateAtLeast(c, SHUTDOWN)) continue retry; // else CAS failed due to workerCount change; retry inner loop } }}
我們很明顯地看到當core為flase的時候咱們獲取的是maximumPoolSize,也就是最大線程數。
寫到這里,其實咱們的核心主流程大概就已經結束了。這里其實老貓也只是寫了一個算是比較入門的開頭。當然我們還可以再深入去理addWorker的源碼。這個其實就交給大家去細看了,篇幅過長,相信大家也會失去閱讀的興趣了,感興趣的可以自己研究一下,如果說還是有問題的,可以找老貓一起探討,老貓的公眾號:"程序員老貓"。老貓覺得在上述的源碼中比較重要的其實就是ctl值的流轉順序以及計算方式,讀懂這個的話,后面一切的源碼只要順藤摸瓜即可理解。
我們上述主要和大家分享了比較核心的theadPoolExecutor。除此之外,線程池Executors里面包含了很多其他的線程池模板。當然這也是小貓直接面試的時候說的那些,其實小貓也就僅僅只是背了線程池模板而已,并不知曉其工作原理。如下幾種:
上述針對這些羅列了一下,其實很多官網上也有相關的介紹,當然感興趣的小伙伴也可以再去刨一刨里面的源碼實現。
很多小伙伴在用一些線程池或者第三方中間件的時候可能只停留在如何使用上,一旦出了問題或者被人深入問到其實現原理的時候就比較頭大。所以在日常開發的過程中,我們不僅僅需要知道如何去用,其實更應該知道底層的原理是什么。這樣才能長立于不敗之地。
本文鏈接:http://www.www897cc.com/showinfo-26-60967-0.html背會了常見的幾個線程池用法,結果被問翻
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 面試官:實際工作中哪里用到了自定義注解?