Java中正確終止執行緒的方法
Thread類中有一個已經廢棄的 stop() 方法,它可以終止執行緒,但由於它不管三七二十一,直接終止執行緒,所以被廢棄了。比如,當執行緒被停止後還需要進行一些善後操作(如,關閉外部資源),使用這個方法就無能為力了。可以通過執行緒中斷來實現執行緒終止。
首先來看一下Java執行緒中斷的一些內容:
- Java平臺為每個執行緒維護了一個布林型的中斷標記,可以通過下列方法獲取該標記的值:
- interrupt() 中斷某個執行緒
- isInterrupted() 返回該執行緒的中斷標記
- interrupted() 返回並重置該執行緒的中斷標記(置為false)
- 中斷僅是發起執行緒對目標執行緒的一種請求,也就是說,目標執行緒對這種請求可以相應,也可以忽略。
- Java標準庫中與執行緒阻塞相關的方法對中斷的相應方式都是丟擲 InterruptedException 異常,並且按照慣例,丟擲異常前都會重置中斷標記為false,因此這些方法會清空執行緒的中斷標記。
- Java標準庫中與執行緒阻塞相關的方法在進行阻塞前會判斷中斷標記是否為true,為true則丟擲異常;如果在阻塞後呼叫中斷方法的話,那麼JVM會設定該執行緒的中斷標記,然後將該執行緒喚醒,因此中斷具有喚醒執行緒的作用。
由上面幾點和第二句加粗的話可知,可以使用執行緒中斷來實現執行緒終止,只要目標執行緒判斷一下中斷標記即可,即使被中斷的執行緒正處於阻塞狀態,也能把他喚醒起來終止;由第一句加粗的話可知,直接使用執行緒中斷實現執行緒終止是存在風險的,因為可能呼叫了一些Java標準庫的阻塞方法,而導致了中斷標記被清空,也就無法獲得中斷標記了(總是false),因此需要自己建立一箇中斷標記配合使用。
如,下面是一個可中斷的任務執行器,他會在每次執行任務前,判斷一下自定i的終止標記和剩餘的任務數(善後);提供的shutdown方法除了將工作執行緒中斷外(主要作用是喚醒可能處於阻塞狀態的任務),還會將終止交集 terminated 置為 true。
執行 main 方法,可以發現,首先會打印出“客戶端呼叫了 shutdown 方法”,然後過了四秒,main執行緒才會終止,可知shutdown方法正確地將目標執行緒終止了。關於“按照慣例,Java標準庫中丟擲InterruptedException異常的和執行緒相關的阻塞方法會清空中斷標記”,可以將條件中的 !interminated 替換成 !Thread.currentThread().isInterrupted(),然後再執行main方法測試,可以發現main執行緒始終無法終止,因為 sleep() 方法清空了中斷標記,所以 !Thread.currentThread().isInterrupted() 始終為true,導致工作執行緒始終無法終止。
public class TerminableTaskRunner { // 儲存要執行的任務 private final BlockingQueue<Runnable> tasks; // 執行緒終止標誌 private volatile boolean terminated; // 剩餘的任務數 private final AtomicInteger count; // 實際執行任務的執行緒 private volatile Thread workThread; public TerminableTaskRunner(int capacity) { this.tasks = new LinkedBlockingDeque<>(capacity); this.count = new AtomicInteger(0); this.workThread = new WorkThread(); workThread.start(); } public void submit(Runnable task) { this.tasks.add(task); this.count.incrementAndGet(); } public void shutdown() { terminated = true; // 執行緒終止標誌,由於中斷標誌可能會被覆蓋,所以需要自己建立一個標誌 if (workThread != null) workThread.interrupt(); // 喚醒執行緒 } private class WorkThread extends Thread { @Override public void run() { Runnable task; try { while (!terminated || tasks.size() >= 1) { task = tasks.take(); try { task.run(); // 可能會清空當前執行緒的中斷標記,如task.run()在內部呼叫的阻塞方法丟擲了InterruptedException } catch (Throwable e) { e.printStackTrace(); } count.decrementAndGet(); } } catch (InterruptedException e) { // 一旦呼叫shutdown且tasks.take()阻塞住,就丟擲該異常,沒有任務要執行,直接終止 workThread = null; } } } public static void main(String[] args) { TerminableTaskRunner taskRunner = new TerminableTaskRunner(4); for (int i = 0; i < 4; i++) { taskRunner.submit(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { System.out.println("客戶端呼叫了 shutdown 方法"); } }); } taskRunner.shutdown(); } }