ThreadPoolExecutor複用流程的一點理解
一、背景
最近在學併發編程的線程池,看了很多文章和視頻,我認爲都沒有講到線程池的核心。大家都說線程池能減少創建線程的消耗和銷燬時間,但是卻不解釋是怎麼做到的,而僅僅只是不斷重複講如果大於核心線程數會怎麼樣,大於最大線程數會這麼樣。。感覺本末倒置了。我覺得得研究一下,不然線程池在我眼裏就是個多餘的東西。
二、前提知識
這個可能只是我自己一直以來的誤解,我一直以爲線程池裏面可以放不同類型的線程類,所以我一很疑惑,就算線程能複用,但是不同線程的類的run方法不一樣啊?怎麼複用?當然我的想法是錯的。線程池中的線程類就是一個,比如ExecutorService pool = Executors.newFixedThreadPool(4); pool.execute(xxx);這個xxx只能是一類,一個相同的類。
三、關鍵代碼
首先我們理清execute這個方法背後的執行順序,我只一一按順序列出,知道關鍵的地方纔會加以特別說明:
-
execute()
-
addworker(w)
-
Work a = new Worker(w)
work重寫了run方法,也就是runwork()
-
a.start()
以上就是一個線程添加到線程池的邏輯順序,這個並不能體現複用。我們接着看,到了a.start()這個一步,線程就會去執行run(),work重寫了run,也就跑到了runwork()裏面:這個是關鍵。
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不爲空
// 第二進進來的時候因爲final執行的原因,task爲null,就會執行gettask了
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);
}
}
本質是什麼呢:可以看見這個線程的run方法是個while循環,其中getTask會被阻塞住,這個我們後面講 ,因此其實就是該線程不會結束。
下面就要看看那getTask在幹什麼了:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
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 {
// 前面的不用管,看着在這行判斷。
// timed 如果當前線程數 大於核心線程數,那麼就爲true
// 當前線程數大於核心線程數,那麼就把當前線程放到隊列中
// 當前線程數小於核心線程數,就去隊列中拿去
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
可以用文字來描述,一個線程execute的過程(假設該線程池核心數量2個),比方說線程A,第一次進來,會添加到work數組中,然後執行run,然後到gettask發現隊列是空的,他的queue.take()因此一直阻塞着(因此線程不會結束),再來個線程B,一樣執行完然後在queue.tak()阻塞,再來一個線程C,因爲現在大於核心線程數,他會經過一個隊列的offer方法(見下面的代碼),把該線程放到隊列中,因此前面兩個線程的take就可以拿到線程了,於是複用了!通過debug就發現這個c線程沒有經過start()了,於是減少了線程創建的時間!妙!
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();
}
// 但第三個線程進入的時候,就大於核心線程數目了,因此會執行offer!
// 明顯的但offer執行失敗了,也就是隊列滿了,就會到下一個else if
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);
}
// 只要不超過max線程,那麼,就還會繼續創建線程,是個全新的線程,直接運行start
else if (!addWorker(command, false))
reject(command);
}
四、小結
回過頭來思考:複用的本質就是,while循環讓run方法不停止,然後配合隊列進行阻塞。