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();
}
}
