4.5 Netty優雅退出機制
你也許已經習慣了使用下面的代碼,使一個線程池退出:
bossGroup.shutdownGracefully();
那么它是如何工作的呢?由于bossGroup是一個線程池,線程池的關閉要求其中的每一個線程關閉。而線程的實現是在SingleThreadEventExecutor類,所以我們將再次回到這個類,首先看其中的shutdownGracefully()方法,其中的參數quietPeriod為靜默時間,timeout為截止時間,此外還有一個相關參數gracefulShutdownStartTime即優雅關閉開始時間,代碼如下:
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
if (isShuttingDown()) {
return terminationFuture(); // 正在關閉阻止其他線程
}
boolean inEventLoop = inEventLoop();
boolean wakeup;
int oldState;
for (;;) {
if (isShuttingDown()) {
return terminationFuture(); // 正在關閉阻止其他線程
}
int newState;
wakeup = true;
oldState = STATE_UPDATER.get(this);
if (inEventLoop) {
newState = ST_SHUTTING_DOWN;
} else {
switch (oldState) {
case ST_NOT_STARTED:
case ST_STARTED:
newState = ST_SHUTTING_DOWN;
break;
default: // 一個線程已修改好線程狀態,此時這個線程才執行16行代碼
newState = oldState;
wakeup = false; // 已經有線程喚醒,所以不用再喚醒
}
}
if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
break; // 保證只有一個線程將oldState修改為newState
}
// 隱含STATE_UPDATER已被修改,則在下一次循環返回
}
// 在default情況下會更新這兩個值
gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
gracefulShutdownTimeout = unit.toNanos(timeout);
if (oldState == ST_NOT_STARTED) {
thread.start();
}
if (wakeup) {
wakeup(inEventLoop);
}
return terminationFuture();
}
這段代碼真是為多線程同時調用關閉的情況操碎了心,我們抓住其中的關鍵點:該方法只是將線程狀態修改為ST_SHUTTING_DOWN并不執行具體的關閉操作(類似的shutdown方法將線程狀態修改為ST_SHUTDOWN)。for()循環是為了保證修改state的線程(原生線程或者外部線程)有且只有一個。如果你還沒有理解這句話,請查閱compareAndSet()方法的說明然后再看一遍。39-44行代碼之所以這樣處理,是因為子類的實現中run()方法是一個EventLoop即一個循環。40行代碼啟動線程可以完整走一遍正常流程并且可以處理添加到隊列中的任務以及IO事件。43行喚醒阻塞在阻塞點上的線程,使其從阻塞狀態退出。要從一個EventLoop循環中退出,有什么好方法嗎?可能你會想到這樣處理:設置一個標記,每次循環都檢測這個標記,如果標記為真就退出。Netty正是使用這種方法,NioEventLoop的run()方法的循環部分有這樣一段代碼:
if (isShuttingDown()) { // 檢測線程狀態
closeAll(); // 關閉注冊的channel
if (confirmShutdown()) {
break;
}
}
查詢線程狀態的方法有三個,實現簡單,一并列出:
public boolean isShuttingDown() {
return STATE_UPDATER.get(this) >= ST_SHUTTING_DOWN;
}
public boolean isShutdown() {
return STATE_UPDATER.get(this) >= ST_SHUTDOWN;
}
public boolean isTerminated() {
return STATE_UPDATER.get(this) == ST_TERMINATED;
}
需要注意的是調用shutdownGracefully()方法后線程狀態為ST_SHUTTING_DOWN,調用shutdown()方法后線程狀態為ST_SHUTDOWN。isShuttingDown()可以一并判斷這兩種調用方法。closeAll()方法關閉注冊到NioEventLoop的所有Channel,代碼不再列出。confirmShutdown()方法在SingleThreadEventExecutor類,確定是否可以關閉或者說是否可以從EventLoop循環中跳出。代碼如下:
protected boolean confirmShutdown() {
if (!isShuttingDown()) {
return false; // 沒有調用shutdown相關的方法直接返回
}
if (!inEventLoop()) { // 必須是原生線程
throw new IllegalStateException("must be invoked from an event loop");
}
cancelScheduledTasks(); // 取消調度任務
if (gracefulShutdownStartTime == 0) { // 優雅關閉開始時間,這也是一個標記
gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();
}
// 執行完普通任務或者沒有普通任務時執行完shutdownHook任務
if (runAllTasks() || runShutdownHooks()) {
if (isShutdown()) {
return true; // 調用shutdown()方法直接退出
}
if (gracefulShutdownQuietPeriod == 0) {
return true; // 優雅關閉靜默時間為0也直接退出
}
wakeup(true); // 優雅關閉但有未執行任務,喚醒線程執行
return false;
}
final long nanoTime = ScheduledFutureTask.nanoTime();
// shutdown()方法調用直接返回,優雅關閉截止時間到也返回
if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) {
return true;
}
// 在靜默期間每100ms喚醒線程執行期間提交的任務
if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {
wakeup(true);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
return false;
}
// 靜默時間內沒有任務提交,可以優雅關閉,此時若用戶又提交任務則不會被執行
return true;
}
我們總結一下,調用shutdown()方法從循環跳出的條件有:
(1).執行完普通任務
(2).沒有普通任務,執行完shutdownHook任務
(3).既沒有普通任務也沒有shutdownHook任務
調用shutdownGracefully()方法從循環跳出的條件有:
(1).執行完普通任務且靜默時間為0
(2).沒有普通任務,執行完shutdownHook任務且靜默時間為0
(3).靜默期間沒有任務提交
(4).優雅關閉截止時間已到
注意上面所列的條件之間是或的關系,也就是說滿足任意一條就會從EventLoop循環中跳出。我們可以將靜默時間看為一段觀察期,在此期間如果沒有任務執行,說明可以跳出循環;如果此期間有任務執行,執行完后立即進入下一個觀察期繼續觀察;如果連續多個觀察期一直有任務執行,那么截止時間到則跳出循環。我們看一下shutdownGracefully()的默認參數:
public Future<?> shutdownGracefully() {
return shutdownGracefully(2, 15, TimeUnit.SECONDS);
}
可知,Netty默認的shutdownGracefully()機制為:在2秒的靜默時間內如果沒有任務,則關閉;否則15秒截止時間到達時關閉。換句話說,在15秒時間段內,如果有超過2秒的時間段沒有任務則關閉。至此,我們明白了從EvnetLoop循環中跳出的機制,最后,我們抵達終點站:線程結束機制。這一部分的代碼實現在線程工廠的生成方法中:
thread = threadFactory.newThread(new Runnable() {
@Override
public void run() {
boolean success = false;
updateLastExecutionTime();
try {
SingleThreadEventExecutor.this.run(); // 模板方法,EventLoop實現
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
for (;;) {
int oldState = STATE_UPDATER.get(SingleThreadEventExecutor.this);
// 用戶調用了關閉的方法或者拋出異常
if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
break; // 拋出異常也將狀態置為ST_SHUTTING_DOWN
}
}
if (success && gracefulShutdownStartTime == 0) {
// time=0,說明confirmShutdown()方法沒有調用,記錄日志
}
try {
for (;;) {
// 拋出異常時,將普通任務和shutdownHook任務執行完畢
// 正常關閉時,結合前述的循環跳出條件
if (confirmShutdown()) {
break;
}
}
} finally {
try {
cleanup();
} finally {
// 線程狀態設置為ST_TERMINATED,線程終止
STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
threadLock.release();
if (!taskQueue.isEmpty()) {
// 關閉時,任務隊列中添加了任務,記錄日志
}
terminationFuture.setSuccess(null); // 異步結果設置為成功
}
}
}
}
});
20-22行代碼說明子類在實現模板方法run()時,須調用confirmShutdown()方法,不調用的話會有錯誤日志。25-31行的for()循環主要是對異常情況的處理,但同時也兼顧了正常調用關閉方法的情況。可以將拋出異常的情況視為靜默時間為0的shutdownGracefully()方法,這樣便于理解循環跳出條件。34行代碼cleanup()的默認實現什么也不做,NioEventLoop覆蓋了基類,實現關閉NioEventLoop持有的selector:
protected void cleanup() {
try {
selector.close();
} catch (IOException e) {
logger.warn("Failed to close a selector.", e);
}
}
關于Netty優雅關閉的機制,還有最后一點細節,那就是runShutdownHooks()方法:
private boolean runShutdownHooks() {
boolean ran = false;
while (!shutdownHooks.isEmpty()) {
// 使用copy是因為shutdwonHook任務中可以添加或刪除shutdwonHook任務
List<Runnable> copy = new ArrayList<Runnable>(shutdownHooks);
shutdownHooks.clear();
for (Runnable task: copy) {
try {
task.run();
} catch (Throwable t) {
logger.warn("Shutdown hook raised an exception.", t);
} finally {
ran = true;
}
}
}
if (ran) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
return ran;
}
此外,還有threadLock.release()方法,如果你還記得字段定義,threadLock是一個初始值為0的信號量。一個初值為0的信號量,當線程請求鎖時只會阻塞,這有什么用呢?awaitTermination()方法揭曉答案,用來使其他線程阻塞等待原生線程關閉 :
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
// 由于tryAcquire()永遠不會成功,所以必定阻塞timeout時間
if (threadLock.tryAcquire(timeout, unit)) {
threadLock.release();
}
return isTerminated();
}