通過 《深入學習線程池之線程池簡介及工作原理》、《深入學習線程池之通過ThreadPoolExecutor創建線程池及工作原理》 兩篇文章,相信大家已經了解怎么去創建一個線程池,并對線程池的工作原理有了認識,但你知道如何去關閉線程池么?直接調用shutdown()方法為什么關不掉線程池呢?shutdownNow()和shutdown()有什么區別?下面我們以ThreadPoolExecutor為例,來介紹下如何優雅的關閉線程池。在介紹線程池關閉之前,先介紹下線程中斷。
一、線程中斷
在程序中,我們不能隨便中斷一個線程,因為這是極其不安全的操作,我們無法知道這個線程正運行在什么狀態,它可能持有某把鎖,強行中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強行中斷導致數據不一致,從而混亂的問題。正因此,Java里將Thread的stop法?設置為過時,以禁止大家使用。
一個線程什么時候可以退出呢?當然只有線程自己才能知道。
所以我們這里要說的Thread的interrrupt方法,本質不是用來中斷一個線程,而是將線程設置一個中斷狀態。當我們調用線程的interrupt方法,它有兩個作用:
1、如果此線程處于阻塞狀態(比如調用了wait方法,io等待),則會立刻退出阻塞,并拋出InterruptedException異常,線程就可以通過捕獲InterruptedException來做一定的處理,然后讓線程退出。
2、如果此線程正處于運行之中,則線程不受任何影響,繼續運行,僅僅是線程的中斷標記被設置為true。所以線程要在適當的位置通過調用isInterrupted方法來查看自己是否被中斷,并做退出操作。
注:如果線程的interrupt方法先被調用,然后線程調用阻塞方法進入阻塞狀態,InterruptedException異常依舊會拋出。如果線程捕獲InterruptedException異常后,繼續調用阻塞方法, 將不再觸發InterruptedException異常。
二、線程池的兩種關閉方式
線程池提供了兩個關閉方法:shuwdown() 和 shutdownNow() 方法。我們都知道這兩個方法的處理邏輯,如下:
shutdown()方法處理邏輯是: 線程池不再接收新提交的任務,同時等待線程池?的任務執行完畢后關閉線程池。
shutdownNow()方法處理邏輯是: 線程池不再接收新提交的任務,同時立刻關閉線程池,線程池里的任務不再執行,并返回待所有未處理的線程list列表。
但是,調用shutdown()方法后,為什么正在執?任務的線程會繼續執行完任務而不是立即停止?調用完shutdown() 或者 shutdownNow()方法后,線程池會立即關閉么?線程在什么情況下才會徹底退出?
如果不了解這些細節,在關閉線程池時就難免遇到,“線程池關閉不了”,“關閉線程池出現報錯” 等情況。下面就結合線程池源碼,分別說說這兩個線程池關閉方法的一些實現細節。
1. 線程池中執行任務的方法runWorker()
/**
* Main worker run loop. Repeatedly gets tasks from queue and
* executes them, while coping with a number of issues:
*
* 1. We may start out with an initial task, in which case we
* don't need to get the first one. Otherwise, as long as pool is
* running, we get tasks from getTask. If it returns null then the
* worker exits due to changed pool state or configuration
* parameters. Other exits result from exception throws in
* external code, in which case completedAbruptly holds, which
* usually leads processWorkerExit to replace this thread.
*
* 2. Before running any task, the lock is acquired to prevent
* other pool interrupts while the task is executing, and then we
* ensure that unless pool is stopping, this thread does not have
* its interrupt set.
*
* 3. Each task run is preceded by a call to beforeExecute, which
* might throw an exception, in which case we cause thread to die
* (breaking loop with completedAbruptly true) without processing
* the task.
*
* 4. Assuming beforeExecute completes normally, we run the task,
* gathering any of its thrown exceptions to send to afterExecute.
* We separately handle RuntimeException, Error (both of which the
* specs guarantee that we trap) and arbitrary Throwables.
* Because we cannot rethrow Throwables within Runnable.run, we
* wrap them within Errors on the way out (to the thread's
* UncaughtExceptionHandler). Any thrown exception also
* conservatively causes thread to die.
*
* 5. After task.run completes, we call afterExecute, which may
* also throw an exception, which will also cause thread to
* die. According to JLS Sec 14.20, this exception is the one that
* will be in effect even if task.run throws.
*
* The net effect of the exception mechanics is that afterExecute
* and the thread's UncaughtExceptionHandler have as accurate
* information as we can provide about any problems encountered by
* user code.
*
* @param w the worker
*/
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) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
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);
}
}
正常情況下,線程池里的線程,就是在這個while循環里不停地執行。其中代碼task.run()就是在執行我們提交給線程池的任務,如下:
threadpool.execute(new Runnable() {
@Override
public void run() {
// todo 具體的業務邏輯
}
});
從runWorker()方法看得出來,如果getTask()方法返回null,會導致線程的退出。我們再來看看getTask()方法的實現:
/**
* Performs blocking or timed wait for a task, depending on
* current configuration settings, or returns null if this worker
* must exit because of any of:
* 1. There are more than maximumPoolSize workers (due to
* a call to setMaximumPoolSize).
* 2. The pool is stopped.
* 3. The pool is shutdown and the queue is empty.
* 4. This worker timed out waiting for a task, and timed-out
* workers are subject to termination (that is,
* {@code allowCoreThreadTimeOut || workerCount > corePoolSize})
* both before and after the timed wait, and if the queue is
* non-empty, this worker is not the last thread in the pool.
*
* @return task, or null if the worker must exit, in which case
* workerCount is decremented
*/
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 {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
2. shutdown()方法
當我們調用shutdown()方法時,源碼如下,我們看到,它先將線程池的狀態修改為SHUTDOWN狀態,然后調用interruptIdleWorkers()方法,來中斷空閑的線程,為什么是空閑線程呢?
在上邊runWorker方法的代碼中,我們看到獲取任務之后第一步是進行加鎖操作,即,w.lock()
。而,shutdown()方法調用的interruptIdleWorkers方法,會嘗試進行w.tryLock()
加鎖操作,換言之,在runWorker方法中w.lock
和w.unlock
之間的線程將因為加鎖成功,就會導致interruptIdleWorkers方法的w.tryLock()
加鎖失敗,進而不會被調用interrupt方法,也就是說正在執行線程池里任務的線程不會被中斷。
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN); // ① 將線程池狀態置為SHUTDOWN
interruptIdleWorkers(); // ② 停用線程池中的線程
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
/**
* Interrupts threads that might be waiting for tasks (as
* indicated by not being locked) so they can check for
* termination or configuration changes. Ignores
* SecurityExceptions (in which case some threads may remain
* uninterrupted).
*
* @param onlyOne If true, interrupt at most one worker. This is
* called only from tryTerminate when termination is otherwise
* enabled but there are still other workers. In this case, at
* most one waiting worker is interrupted to propagate shutdown
* signals in case all threads are currently waiting.
* Interrupting any arbitrary thread ensures that newly arriving
* workers since shutdown began will also eventually exit.
* To guarantee eventual termination, it suffices to always
* interrupt only one idle worker, but shutdown() interrupts all
* idle workers so that redundant workers exit promptly, not
* waiting for a straggler task to finish.
*/
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {// 線程沒有被中斷且worker獲取到鎖的時候才處理
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
/**
* Common form of interruptIdleWorkers, to avoid having to
* remember what the boolean argument means.
*/
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
3. shutdownNow()方法
當我們調用shutdownNow()方法時,源碼如下,我們看到,它先將線程池的狀態修改為STOP狀態,然后調用interruptWorkers()方法,遍歷中斷線程,最后返回未執行的任務的線程list。
在runWorker方法中,代碼task.run()就是在執行我們提交給線程池的任務,當我們調用shutdownNow方法時,task.run()里面正處于IO阻塞,即,我們提交任務的邏輯,涉及到IO阻塞,則會導致報錯,如果task.run()里正在正常執行,則不受影響,繼續執行完這個任務。
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
*
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
*
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
*
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
/**
* Interrupts all threads, even if active. Ignores SecurityExceptions
* (in which case some threads may remain uninterrupted).
*/
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
總結
一、當我們調用線程池的shutdownNow方法時,會將線程池狀態修改為STOP,當執行runWorker方法中while (task != null || (task = getTask()) != null)
時,在getTask方法中,由于STOP狀態值是大于SHUTDOWN狀態,STOP也大于等于STOP,所以不管任務隊列是否為空,都會進入if語句,即,
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
從而返回null,導致 (task = getTask()) != null
條件不成立,進而執行線程退出。
二、當我們調用線程池的shuwdown方法時,會將線程池狀態修改為SHUTDOWN,當執行runWorker方法中while (task != null || (task = getTask()) != null)
時,在getTask方法中,SHUTDOWN大于等于SHUTDOWN成立沒問題,但是SHUTDOWN不大于等于STOP狀態,所以只有隊列為空,getTask方法才會返回null,導致線程退出。如果線程正在執行線程池里的任務,即便任務處于阻塞狀態,線程也不會被中斷,而是繼續執行。如果線程池阻塞等待從隊列里讀取任務,則會被喚醒,但是會繼續判斷隊列是否為空,若不為空,則會繼續從隊列里讀取任務,若為空則線程退出。
優雅的關閉線程池
使用shutdownNow?法,可能會引起報錯,使用shutdown方法可能會導致線程關閉不了。
所以當我們使用shutdownNow?法關閉線程池時,一定要對任務里進行異常捕獲。即,在我們提交的任務里有try{}catch{}處理
當我們使用shuwdown方法關閉線程池時,一定要確保任務里不會有永久阻塞等待的邏輯,否則線程池就關閉不了。
最后,一定要記得shutdownNow和shuwdown調用完,線程池并不是立刻就關閉了,要想等待線程池關閉,還需調用awaitTermination方法來阻塞等待。