背景
作為一名 Java 開發者,對執行緒池絕對不陌生,無論是平時工作,還是面試都會,執行緒池都是必會的知識點.而且不能不能之知其表面,理解不透徹,很容易在實戰中出現 OOM,也可能在面試中被問趴?.
引數含義
其實,如果研究過執行緒池的話,其實並不難,他的引數並不多,java.util.concurrent.ThreadPoolExecutor
中的引數列舉出來就是這些.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
複製程式碼
- corePoolSize:執行緒池中的核心執行緒數,即使沒有任務執行的時候,他們也是存在的.(不考慮配置了引數:allowCoreThreadTimeOut,allowCoreThreadTimeOut通過字面意思也能知道,就是是否允許核心執行緒超時,一般情況下不需要設定,本文不考慮)
- maximumPoolSize:執行緒池中的允許存在的最大執行緒數.
- keepAliveTime: 當執行緒池中的執行緒超過核心執行緒數的時候,這部分多餘的空閒執行緒等待執行新任務的超時時間.例如:核心執行緒數為1 ,最大執行緒數為5,當前執行執行緒為4,keepAliveTime為60s,那麼4-1=3個執行緒在空閒狀態下等待60s 後還沒有新任務到來,就會被銷燬了.
- unit :keepAliveTime 的時間單位.
- workQueue: 執行緒佇列,如果當前時間核心執行緒都在執行,又來了一個新任務,那麼這個新任務就會被放進這個執行緒佇列中,等待執行.
- threadFactory: 執行緒池建立執行緒的工廠類.
- handler: 如果執行緒佇列滿了同事執行執行緒數也達到了maximumPoolSize,如果此時再來新的執行緒,將執行什麼 handler 來處理這個執行緒. handler的預設提供的型別有:
- AbortPolicy: 丟擲
RejectedExecutionException
異常 - DiscardPolicy: 什麼都不做.
- DiscardOldestPolicy: 將執行緒佇列中的最老的任務拋棄掉,換區一個空間執行當前的任務.
- CallerRunsPolicy: 使用當前的執行緒(比如
main
)來執行這個執行緒.
- AbortPolicy: 丟擲
執行流程
我們知道了引數的含義,那麼這些引數在執行過程中到底是怎麼執行的呢,我們先用文字分幾種情況來描述一下.
在說之前,先來看一個例子.
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new BasicThreadFactory.Builder().namingPattern("name-%d").build());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("test");
}
});
複製程式碼
不得不說,在很長一段時間內,我都有一個疑問或者說誤區,明明是執行緒池,為什麼每次都需要我 new 一個 執行緒(錯誤)
呢? 因為我們開始學執行緒的時候先學 new Thread()
,後來又學了new Runnable()
,慢慢的就把這兩個混為一罈了,其實 new Runnable()
並沒有新起一個執行緒,只是新建了一個可執行的任務,就是一個普通的物件而已,哈哈,這應該是一個很傻的錯誤認知.
回到上面說的具體含義.
- 如果新加入一個執行的任務,當前執行的執行緒小於
corePoolSize
,這時候會線上程池中新建一個執行緒用於執行這個新的任務. - 如果新加入一個執行的任務,當前執行的執行緒大於等於
corePoolSize
,這個時候就需要將這個新的任務加入到執行緒佇列workQueue
中,一旦執行緒中的執行緒執行完成了一個任務,就會馬上從佇列中去一個任務來執行. - 接2,如果佇列也滿了,怎麼辦呢? 如果
maximumPoolSize
大於corePoolSize
,就會新建執行緒來處理這個新的任務,知道總執行執行緒數達到maximumPoolSize
. - 如果總執行執行緒數達到了
maximumPoolSize
,還來了新的任務怎麼辦呢?就需要執行上面所說的拒絕策略了handler
了,按照配置的策略進行處理,預設不配置的情況下,使用的是AbortPolicy
.
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
複製程式碼
原始碼驗證
怎麼判斷上面說的流程是正確的呢?我們可以跟進原始碼來仔細檢視一下上面的流程,其實執行緒池執行的程式碼比較簡單,一看變動,看了原始碼,掌握得應該會更加深刻一些.
首先來看看execute()
方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// ctl是一個原子的控制位,可以表示執行緒池的狀態和執行的執行緒數;
int c = ctl.get();
// 1. 如果執行執行緒數小於核心執行緒數
if (workerCountOf(c) < corePoolSize) {
//直接新建 worker(執行緒)執行.
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. 如果上面的addWorker 失敗了,就需要加入執行緒佇列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 加入後,檢查狀態;
if (! isRunning(recheck) && remove(command))
//檢查執行狀態不通過,移除任務,執行拒絕策略
reject(command);
// 如果當前的執行執行緒為0
else if (workerCountOf(recheck) == 0)
//就是用核心執行緒執行剛剛新增到佇列的執行緒
addWorker(null, false);
}
// 3. 如果佇列也滿了,就新建執行緒繼續處理
else if (!addWorker(command, false))
// 4. 如果不允許新建了,就執行拒絕策略
reject(command);
}
複製程式碼
按照一個正常流程來說,我們只考慮一個理想的環境.我們可以分為上面的4步,正好和上面的文字描述對應.
可能愛思考的同學發現,第2步,加入佇列後,什麼時候執行這個新加入的呢,難道有一個定時任務嗎?並不是.我們可以看看這個addWorker()
方法.
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
//第一層迴圈
for (;;) {
int c = ctl.get();
//獲取當前執行緒池的狀態;
int rs = runStateOf(c);
...
//第二層迴圈
for (;;) {
//獲取執行緒池的執行執行緒個數
int wc = workerCountOf(c);
// 大於了最大允許的執行緒個數,當然要返回 false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通過了檢查,就把 正在執行的執行緒數加1
if (compareAndIncrementWorkerCount(c))
//跳出第一層迴圈
break retry;
c = ctl.get(); // Re-read ctl
//加1 失敗,可能有多執行緒衝突,檢查一下最新的狀態,繼續重試;
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//新建一個執行緒包裝我們的 Runnable
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
...
//加入 hashSet 中管理存在於執行緒池中執行緒
workers.add(w);
workerAdded = true;
if (workerAdded) {
// 啟動 worker,worker就是執行緒中真正執行的執行緒,包裝了我們提供的 Runnable
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
複製程式碼
上面的addWorker()
方法中,就是靠t.start()
來啟動執行緒的.
Worker
這個類存在於java.util.concurrent.ThreadPoolExecutor.Worker
,定義如下
只保留了相對重要的程式碼.
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);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
....
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
...
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
}
複製程式碼
所以當t.start()
的時候,實際上,新建了一個執行緒,執行了runWorker(this);
方法:
這個方法裡面有一個while
迴圈,getTask()
是從佇列中獲取一個任務.所以說這裡可以解答上面放到佇列裡面的任務什麼時候執行了,等到任意一個核心執行緒空閒出來時候,他就會迴圈去取佇列中的任務執行.每個核心執行緒和新起來的執行緒都是同步來執行你傳進來的Runnable
的run
方法.
整個流程應該就比較清楚了.
上面說了這麼多,核心引數都說的差不多了,那麼keepAliveTime 這個引數在原始碼怎麼來用的呢?
上面說到一個getTask()
方法從佇列中取一個任務,看一下這個方法的程式碼(省略非主要的).
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
...
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
複製程式碼
主要就是用於取任務這裡,poll()
不會阻塞,take()
是阻塞的,所以當使用poll
取資料的時候,到達設定的超時,就會繼續往下執行,如果超過設定時間還是沒有任務進來,就會將timedOut
設定為 true,返回 null.
這個timedOut
會控制上面的 if
判斷,最終控制compareAndDecrementWorkerCount()
方法,就是講執行的執行緒數減1個,那麼下次如果又滿了,就會新建一個,所以這個 Alive 就失效了.
總結
總體來說,通過原始碼來看問題能比較權威的解答一些問題.有時候原始碼似乎也沒有那麼高深?