【Java併發程式設計】阿里最喜歡問的幾道執行緒池的面試題?
阿新 • • 發佈:2020-12-30
### 引言
上一篇文章我們有介紹過執行緒池的一個基本執行流程[《【Java併發程式設計】面試必備之執行緒池》](https://mp.weixin.qq.com/s/9l2l2whLgYPrBbsGv3Xw6w)以及它的7個核心引數,以及每個引數的作用、以及如何去使用執行緒池
還留了幾個小問題。。建議看這篇文章之前可先看下前面那篇文章。這篇文章我們就來分析下上篇文章的幾個小問題
- 執行緒池是否區分核心執行緒和非核心執行緒?
- 如何保證核心執行緒不被銷燬?
- 執行緒池的執行緒是如何做到複用的?
我們先看最後一個問題一般一個執行緒執行完任務之後就結束了,`Thread.start()`只能呼叫一次,一旦這個呼叫結束,則該執行緒就到了`stop`狀態,不能再次呼叫`start`。如果你對一個已經啟動的執行緒物件再呼叫一次`start`方法的話,會產生:`IllegalThreadStateException`異常,但是`Thread`的`run`方法是可以重複呼叫的。所以這裡也會有一個面試經常問到的問題:**Thread類中run()和start()方法的有什麼區別?**
下面我們就從`jdk`的原始碼來一起看看如何實現執行緒複用的:
執行緒池執行任務的`ThreadPoolExecutor`#`execute`方法為入口
```java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 執行緒池當前執行緒數小於 corePoolSize 時進入if條件呼叫 addWorker 建立核心執行緒來執行任務
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 執行緒池當前執行緒數大於或等於 corePoolSize ,就將任務新增到 workQueue 中
if (isRunning(c) && workQueue.offer(command)) {
// 獲取到當前執行緒的狀態,賦值給 recheck ,是為了重新檢查狀態
int recheck = ctl.get();
// 如果 isRunning 返回 false ,那就 remove 掉這個任務,然後執行拒絕策略,也就是回滾重新排隊
if (! isRunning(recheck) && remove(command))
reject(command);
// 執行緒池處於 running 狀態,但是沒有執行緒,那就建立執行緒執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果任務放入 workQueue 失敗,則嘗試通過建立非核心執行緒來執行任務
// 建立非核心執行緒失敗,則說明執行緒池已經關閉或者已經飽和,會執行拒絕策略
else if (!addWorker(command, false))
reject(command);
}
```
**excute**方法主要業務邏輯
- 如果當前的執行緒池執行執行緒小於**coreSize**,則建立新執行緒來執行任務。
- 如果當前執行的執行緒等於**coreSize**或多餘**coreSize**(動態修改了coreSize才會出現這種情況),把任務放到阻塞佇列中。
- 如果佇列已滿無法將新加入的任務放進去的話,則需要建立新的執行緒來執行任務。
- 如果新建立執行緒已經達到了最大執行緒數,任務將會被拒絕。
#### addWorker 方法
上述方法的核心主要就是addWorker方法,
```java
private boolean addWorker(Runnable firstTask, boolean core) {
// 前面還有一部分就省略了。。。。
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
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;
}
```
這個方法我們先看看這個**work**類吧
```java
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
```
**work**類實現了**Runnable**介面,然後run方法裡面呼叫了**runWorker**方法
```java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 新增建立
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 判斷 task 是否為空,如果不為空直接執行
// 如果 task 為空,呼叫 getTask() 方法,從 workQueue 中取出新的 task 執行
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
```
這個`runwork`方法中會優先取`worker`繫結的任務,如果建立這個worker的時候沒有給`worker`繫結任務,`worker`就會從佇列裡面獲取任務來執行,執行完之後`worker`並不會銷燬,而是通過`while`迴圈不停的執行`getTask`方法從阻塞佇列中獲取任務呼叫task.run()來執行任務,這樣的話就達到了執行緒複用的目的。 `while (task != null || (task = getTask()) != null)` 這個迴圈條件只要`getTask` 返回獲取的值不為空這個迴圈就不會終止, 這樣執行緒也就會一直在執行。
**那麼任務執行完怎麼保證核心執行緒不銷燬?非核心執行緒銷燬?**
答案就在這個`getTask()`方法裡面
```java
private Runnable getTask() {
// 超時標記,預設為false,如果呼叫workQueue.poll()方法超時了,會標記為true
// 這個標記非常之重要,下面會說到
boolean timedOut = false;
for (;;) {
// 獲取ctl變數值
int c = ctl.get();
int rs = runStateOf(c);
// 如果當前狀態大於等於SHUTDOWN,並且workQueue中的任務為空或者狀態大於等於STOP
// 則操作AQS減少工作執行緒數量,並且返回null,執行緒被回收
// 也說明假設狀態為SHUTDOWN的情況下,如果workQueue不為空,那麼執行緒池還是可以繼續執行剩下的任務
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 操作AQS將執行緒池中的執行緒數量減一
decrementWorkerCount();
return null;
}
// 獲取執行緒池中的有效執行緒數量
int wc = workerCountOf(c);
// 如果主動開啟allowCoreThreadTimeOut,或者獲取當前工作執行緒大於corePoolSize,那麼該執行緒是可以被超時回收的
// allowCoreThreadTimeOut預設為false,即預設不允許核心執行緒超時回收
// 這裡也說明了在核心執行緒以外的執行緒都為“臨時”執行緒,隨時會被執行緒池回收
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 這裡說明了兩點銷燬執行緒的條件:
// 1.原則上執行緒池數量不可能大於maximumPoolSize,但可能會出現併發時操作了setMaximumPoolSize方法,如果此時將最大執行緒數量調少了,很可能會出現當前工作執行緒大於最大執行緒的情況,這時就需要執行緒超時回收,以維持執行緒池最大執行緒小於maximumPoolSize,
// 2.timed && timedOut 如果為true,表示當前操作需要進行超時控制,這裡的timedOut為true,說明該執行緒已經從workQueue.poll()方法超時了
// 以上兩點滿足其一,都可以觸發執行緒超時回收
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 嘗試用AQS將執行緒池執行緒數量減一
if (compareAndDecrementWorkerCount(c))
// 減一成功後返回null,執行緒被回收
return null;
// 否則迴圈重試
continue;
}
try {
// 如果timed為true,阻塞超時獲取任務,否則阻塞獲取任務
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// 如果poll超時獲取任務超時了, 將timeOut設定為true
// 繼續迴圈執行,如果碰巧開發者開啟了allowCoreThreadTimeOut,那麼該執行緒就滿足超時回收了
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
```
所以保證執行緒不被銷燬的關鍵程式碼就是這一句程式碼
```java
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
```
只要`timed`為`false`這個`workQueue.take()`就會一直阻塞,也就保證了執行緒不會被銷燬。`timed`的值又是通過`allowCoreThreadTimeOut`和正在執行的執行緒數量是否大於`coreSize`控制的。
- 只要`getTask`方法返回`null` 我們的執行緒就會被回收(`runWorker`方法會呼叫`processWorkerExit`)
- 這個方法的原始碼也就解釋了為什麼我們在建立執行緒池的時候設定了`allowCoreThreadTimeOut` =`true`的話,核心執行緒也會進行銷燬。
- 通過這個方法我也們可以回答上面那個問題執行緒池是不區分核心執行緒和非核心執行緒的。
### 結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。
巨人的肩膀摘蘋果
http://objcoding.com/2019/04/25/threadpool-running/