寫在開頭
面試官:“小夥子,執行緒池使用過嗎,來聊一聊它吧!”
我:“好的,然後巴拉巴拉一頓輸出之前看過的build哥執行緒池十八問...”
面試官滿意的點了點頭,緊接著問道:“那你知道如何優雅的關閉執行緒池嗎?”
我:“知道知道,直接呼叫shutdownNow()方法就好了呀!”
面試官臉色一變,微怒道:“粗魯!你給我滾出去!!!”
優雅的關閉執行緒池
哈哈,上面的場景是build哥臆想出來的面試畫面,我們現在步入正題,來看一看線上程池使用完成後如何優雅的關閉執行緒池。
在JDK 1.8 中,Java 併發工具包中 java.util.concurrent.ExecutorService 提供了 shutdown()、shutdownNow()這兩種介面方法去關閉執行緒池,我們分別看一下。
shutdown()
public void shutdown() {
final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主鎖
mainLock.lock(); // 加鎖以確保獨佔訪問
try {
checkShutdownAccess(); // 檢查是否有關閉的許可權
advanceRunState(SHUTDOWN); // 將執行器的狀態更新為SHUTDOWN
interruptIdleWorkers(); // 中斷所有閒置的工作執行緒
onShutdown(); // ScheduledThreadPoolExecutor中的掛鉤方法,可供子類重寫以進行額外操作
} finally {
mainLock.unlock(); // 無論try塊如何退出都要釋放鎖
}
tryTerminate(); // 如果條件允許,嘗試終止執行器
}
在shutdown的原始碼中,會啟動一次順序關閉,在這次關閉中,執行器不再接受新任務,但會繼續處理佇列中的已存在任務,當所有任務都完成後,執行緒池中的執行緒會逐漸退出。
我們寫一個小的demo來使用shutdown():
public class TestService{
public static void main(String[] args) {
//建立固定 3 個執行緒的執行緒池,測試使用,工作中推薦ThreadPoolExecutor
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//向執行緒池提交 10 個任務
for (int i = 1; i <= 10; i++) {
final int index = i;
threadPool.submit(() -> {
System.out.println("正在執行任務 " + index);
//休眠 3 秒,模擬任務執行
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//休眠 4 秒
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//關閉執行緒池
threadPool.shutdown();
}
}
在這段測試程式碼中,我們構造了一個包含固定3執行緒數的執行緒池,迴圈提交10個任務,每個任務休眠3秒,但主程式休眠4秒後,會掉用shutdown方法,理論上,在第二個時間迴圈中,執行緒池被停止,所以最多執行完6個任務,但從輸出中,我們絲毫感受不好執行緒何時被停止了。
輸出:
正在執行任務 1
正在執行任務 3
正在執行任務 2
正在執行任務 4
正在執行任務 5
正在執行任務 6
正在執行任務 7
正在執行任務 8
正在執行任務 9
正在執行任務 10
shutdownNow()
/**
* 嘗試停止所有正在執行的任務,停止處理等待的任務,
* 並返回等待處理的任務列表。
*
* @return 從未開始執行的任務列表
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks; // 用於儲存未執行的任務的列表
final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主鎖
mainLock.lock(); // 加鎖以確保獨佔訪問
try {
checkShutdownAccess(); // 檢查是否有關閉的許可權
advanceRunState(STOP); // 將執行器的狀態更新為STOP
interruptWorkers(); // 中斷所有工作執行緒
tasks = drainQueue(); // 清空佇列並將結果放入任務列表中
} finally {
mainLock.unlock(); // 無論try塊如何退出都要釋放鎖
}
tryTerminate(); // 如果條件允許,嘗試終止執行器
return tasks; // 返回佇列中未被執行的任務列表
}
與shutdown不同的是shutdownNow會嘗試終止所有的正在執行的任務,清空佇列,停止失敗會丟擲異常,並且返回未被執行的任務列表。
由於shutdownNow會有返回值,所以我們將上面的測試案例稍作改動後輸出結果為:
這種會在控制檯丟擲異常的方式,同樣也不優雅,所以我們繼續往下看!
shutdown()+awaitTermination(long timeout, TimeUnit unit)
awaitTermination(long timeout, TimeUnit unit)是可以允許我們在呼叫shutdown方法後,再設定一個等待時間,如設定為5秒,則表示shutdown後5秒內執行緒池徹底終止,返回true,否則返回false;
這種方式裡,我們將shutdown()結合awaitTermination(long timeout, TimeUnit unit)方法去使用,注意在呼叫 awaitTermination() 方法時,應該設定合理的超時時間,以避免程式長時間阻塞而導致效能問題,而且由於這個方法在超時後也會丟擲異常,因此,我們在使用的時候要捕獲並處理異常!
public class TestService{
public static void main(String[] args) {
//建立固定 3 個執行緒的執行緒池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//向執行緒池提交 10 個任務
for (int i = 1; i <= 10; i++) {
final int index = i;
threadPool.submit(() -> {
System.out.println("正在執行任務 " + index);
//休眠 3 秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//關閉執行緒池,設定等待超時時間 3 秒
System.out.println("設定執行緒池關閉,等待 3 秒...");
threadPool.shutdown();
try {
boolean isTermination = threadPool.awaitTermination(3, TimeUnit.SECONDS);
System.out.println(isTermination ? "執行緒池已停止" : "執行緒池未停止");
} catch (InterruptedException e) {
e.printStackTrace();
}
//再等待超時時間 20 秒
System.out.println("再等待 20 秒...");
try {
boolean isTermination = threadPool.awaitTermination(20, TimeUnit.SECONDS);
System.out.println(isTermination ? "執行緒池已停止" : "執行緒池仍未停止,請檢查!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
輸出:
設定執行緒池關閉,等待 3 秒...
正在執行任務 1
正在執行任務 2
正在執行任務 3
正在執行任務 4
正在執行任務 5
執行緒池未停止
再等待 20 秒...
正在執行任務 6
正在執行任務 7
正在執行任務 8
正在執行任務 9
正在執行任務 10
執行緒池已停止
從輸出中我們可以看到,透過將兩種方法結合使用,我們監控了整個執行緒池關閉的全流程,實現了優雅的關閉!
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!