每日三道面試題,通往自由的道路12——執行緒池
茫茫人海千千萬萬,感謝這一秒你看到這裡。希望我的面試題系列能對你的有所幫助!共勉!
願你在未來的日子,保持熱愛,奔赴山海!
每日三道面試題,成就更好自我
昨天既然聊到執行緒池中的實現方式,有些比較重要的我還沒問到。
1. 你知道ThreadPoolExecutor的構造方法和引數嗎
我們先來看看它的構造方法有哪些:
// 五個引數的建構函式 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {...} // 六個引數的建構函式 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {...} // 六個引數的建構函式 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {...} // 七個引數的建構函式 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {...}
我們再來詳解下構造方法中涉及的7個引數,其中最重要5個引數就是第一個構造方法中的。
-
int corePoolSize:該執行緒池中核心執行緒數量
核心執行緒:執行緒池中有兩類執行緒,核心執行緒和非核心執行緒。核心執行緒預設情況下會一直存在於執行緒池中,即使這個核心執行緒什麼都不幹,而非核心執行緒(臨時工)如果長時間的閒置,就會被銷燬。但是如果將
allowCoreThreadTimeOut
設定為true時,核心執行緒也是會被超時回收。 -
int maximumPoolSize:該執行緒池中允許存在的工作執行緒的最大數量。
該值相當於核心執行緒數量 + 非核心執行緒數量。
-
long keepAliveTime
非核心執行緒如果處於閒置狀態超過該值,就會被銷燬。如果設定
allowCoreThreadTimeOut(true)
,則會也作用於核心執行緒。 -
TimeUnit unit:keepAliveTime的時間單位。
TimeUnit是一個列舉型別 ,包括以下屬性:
NANOSECONDS : 1微毫秒 MICROSECONDS : 1微秒 MILLISECONDS : 1毫秒 SECONDS : 秒 MINUTES : 分 HOURS : 小時 DAYS : 天
-
BlockingQueue workQueue:阻塞佇列,維護著等待執行的Runnable任務物件。
當新任務來的時候,會先判斷當前執行執行緒數量是否達到了核心執行緒數,如果達到了,就會被存放在阻塞佇列中排隊等待執行。
常用的幾個阻塞佇列:
-
ArrayBlockingQueue
陣列阻塞佇列,底層資料結構是陣列,需要指定佇列的大小。
-
SynchronousQueue
同步佇列,內部容量為0,每個put操作必須等待一個take操作,反之亦然。
-
DelayQueue
延遲佇列,該佇列中的元素只有當其指定的延遲時間到了,才能夠從佇列中獲取到該元素 。
-
LinkedBlockingQueue
鏈式阻塞佇列,底層資料結構是連結串列,預設大小是
Integer.MAX_VALUE
,也可以指定大小。
-
還有兩個非必須的引數:
-
ThreadFactory threadFactory
建立執行緒的工廠 ,用於批量建立執行緒,統一在建立執行緒時設定一些引數,如是否守護執行緒、執行緒的優先順序等。如果不指定,會新建一個預設的執行緒工廠。
-
RejectedExecutionHandler handler
拒絕處理策略,線上程數量大於最大執行緒數後就會採用拒絕處理策略,四種拒絕處理的策略為 :
- ThreadPoolExecutor.AbortPolicy:預設拒絕處理策略,丟棄任務並丟擲
RejectedExecutionException
異常。 - ThreadPoolExecutor.DiscardPolicy:丟棄新來的任務,但是不丟擲異常。
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列頭部(最舊的)的任務,然後重新嘗試執行程式(如果再次失敗,重複此過程)。
- ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務。
- ThreadPoolExecutor.AbortPolicy:預設拒絕處理策略,丟棄任務並丟擲
不錯呀!執行緒池的引數也有深入瞭解,那咱們繼續
2. 你可以說下執行緒池的執行過程原理嗎
昨天MyGirl跟我講了一下她去銀行辦理業務的一個場景:
- 首先MyGirl(任務A)先去銀行(執行緒池)辦理業務,她發現她來早了,現在銀行才剛開門,櫃檯視窗服務員還沒過來(相當於執行緒池中的初始執行緒為0),此時銀行經理看到MyGirl來了,就安排她去一號櫃檯視窗並安排了1號正式工作人員來接待她。
- 在MyGirl的業務還沒辦完時,一個不知名的路人甲(任務B)出現了,他也是要來銀行辦業務,於是銀行經理安排他去二號櫃檯並安排了2號正式工作人員。假設該銀行的櫃檯視窗就只有兩個(核心執行緒數量2)。
- 緊接著,在所有人業務都還沒做完的情況,持續來個三個不知名的路人乙丙丁,他們也是要來辦業務的,但是由於櫃檯滿了,安排了他們去旁邊的銀行大廳的座位上(阻塞佇列,這裡假設大小為3)等候並給了對應順序的號碼,說等前面兩個人辦理完後,按順序叫號你們呦,請注意聽。
- 過一會,一個路人戊也想來銀行辦理業務,而經理看到櫃檯滿了,座位滿了,只能安排了一個臨時工(非核心執行緒,這裡假設最大執行緒為3,即非核心為1)手持pad裝置並給路人戊去辦理業務。
- 而此時,一個路人戌過來辦理業務,而經理看到櫃檯滿了,座位滿了,臨時工也安排滿了(最大執行緒數+阻塞佇列都滿了),無奈經理只能掏出一本《如何接待超出最大限度的手冊》,選擇拒接接待路人戌通知他,過會再來吧您嘞,這裡已經超負荷啦!
- 最後,相繼所有人的業務都辦完了,現在也沒人再來辦業務,並且臨時工的空閒時間也超過了1小時以上了(最大空閒時間預設60秒),經理讓臨時工都先下班回家了(銷燬執行緒)。
- 但是一個銀行要保證正常的執行,只能讓正式員工繼續上班,不得提早下班。
而實際上執行緒的流程原理跟這個一樣,我們來看下處理任務的核心方法execute
,它的原始碼大概是什麼樣子的呢,當然我們也可以看原始碼中的註釋,裡面也寫的很清楚。這裡具體講下思路。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 1. 獲取ctl,ctl是記錄著執行緒池狀態和執行緒數。
int c = ctl.get();
// 2. 判斷當前執行緒數小於corePoolSize核心執行緒,則呼叫addWorker建立核心執行緒執行任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
// 建立執行緒失敗,需要重新獲取clt的狀態和執行緒數。
c = ctl.get();
}
// 3. 如果不小於corePoolSize,進入下面的方法。
// 判斷執行緒池是否執行狀態並且執行執行緒數大於corePoolSize,將任務新增到workQueue佇列。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 3.1 再次檢查執行緒池是否執行狀態。
// 如果isRunning返回false(狀態檢查),則remove這個任務,然後執行拒絕策略。
if (! isRunning(recheck) && remove(command))
reject(command);
// 3.2 執行緒池處於running狀態,但是沒有執行緒,則建立執行緒加入到執行緒池中
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 4. 如果放入workQueue失敗,則建立非核心執行緒執行任務,
// 如果這時建立非核心執行緒失敗(當前執行緒總數不小於maximumPoolSize時),就會執行拒絕策略。
else if (!addWorker(command, false))
reject(command);
}
我們可以大概看下思路圖:
先解釋下ctl
變數ctl定義為AtomicInteger,記錄了“執行緒池中的任務數量”和“執行緒池的狀態”兩個資訊。以高三位記錄著執行緒池的狀態和低29位記錄執行緒池中的任務數量。
RUNNING : 111
SHUTDOWN : 000
STOP : 001
TIDYING : 010
TERMINATED : 011
最後總結一下執行過程:
- 任務到達時,會先判斷核心執行緒是否滿了,不滿則呼叫addWorker方法建立核心執行緒執行任務。
- 然後會判斷下執行緒池中的執行緒數 < 核心執行緒,無論執行緒是否空閒,都會新建一個核心執行緒執行任務(讓核心執行緒數量快速達到核心執行緒總數)。此步驟會開啟鎖
mainLock.lock();
。 - 而線上程池中的執行緒數 >= 核心執行緒時,新來的執行緒任務會進入任務阻塞佇列中等待,然後空閒的核心執行緒會依次去阻塞佇列中取任務來執行。
- 當阻塞佇列滿了,說明這個時候任務很多了,此時就需要一些非核心執行緒臨時工來執行這些任務了。於是會建立非核心執行緒去執行這個任務。
- 最後當阻塞佇列滿了, 且匯流排程數達到了maximumPoolSize,則會採取拒絕策略進行處理。
- 當非核心執行緒取任務的時間達到keepAliveTime還沒有取到任務即空閒時間,就會回收非核心執行緒。
不錯,這個執行過程原理都有深入瞭解過,最後問你一道:
3. 能否寫一個簡單執行緒池的demo?
你這怕不是魔鬼吧,寫一個執行緒池。不過簡單的執行緒池還是可以寫寫滴!當然通過上面引數,執行過程的學習,寫出來一個還是比較So Easy的。只是如果真的到面試了,真的讓你手敲,可能就忘了,還是得多敲。
這裡還是直接用簡單的ThreadPoolExecutor建立吧,等後續寫執行緒池相關文章,再詳細寫自己建立的執行緒池吧。
我們先建立一個任務類Task:
/**
* 自定義任務類
*/
public class Task implements Runnable{
private int id;
public Task(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "即將執行的任務是" + id + "任務");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "執行完成的任務是" + id + "任務");
}
}
測試程式碼:
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 3;
private static final int MAX_POOL_SIZE = 5;
private static final int QUEUE_CAPACITY = 10;
private static final Long KEEP_ALIVE_TIME = 1l;
public static void main(String[] args) {
//通過ThreadPoolExecutor建構函式自定義引數建立
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Task task = new Task( i);
//執行Runnable
executor.execute(task);
}
//終止執行緒池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("執行緒已經全部執行完");
}
}
得到的結果:
pool-1-thread-1即將執行的任務是0任務
pool-1-thread-3即將執行的任務是2任務
pool-1-thread-2即將執行的任務是1任務
pool-1-thread-1執行完成的任務是0任務
pool-1-thread-3執行完成的任務是2任務
pool-1-thread-1即將執行的任務是3任務
pool-1-thread-3即將執行的任務是4任務
pool-1-thread-2執行完成的任務是1任務
pool-1-thread-2即將執行的任務是5任務
pool-1-thread-3執行完成的任務是4任務
pool-1-thread-1執行完成的任務是3任務
pool-1-thread-3即將執行的任務是6任務
pool-1-thread-1即將執行的任務是7任務
pool-1-thread-2執行完成的任務是5任務
pool-1-thread-2即將執行的任務是8任務
pool-1-thread-3執行完成的任務是6任務
pool-1-thread-1執行完成的任務是7任務
pool-1-thread-3即將執行的任務是9任務
pool-1-thread-2執行完成的任務是8任務
pool-1-thread-3執行完成的任務是9任務
執行緒已經全部執行完
當然此版寫的稍微簡單,但是如果真的忘記,也可以這麼寫。如果後續想看更多東西,可以關注我呀,我會持續更新內容!
小夥子不錯嘛!今天就到這裡,期待你明天的到來,希望能讓我繼續保持驚喜!
參考資料:執行緒池原理
注: 如果文章有任何錯誤和建議,請各位大佬盡情留言!如果這篇文章對你也有所幫助,希望可愛親切的您給個三連關注下,非常感謝啦!