kill 掉一個線程,感覺是一件很簡單的事情,比如 JAVA 中為我們提供了 stop 方法可以立即終止線程的執行,達到 kill 掉線程的目的。
但實際上對線程的操作是一件精細活,對於一段正在執行的任務,我們不能只是簡單粗暴的勒令其停止。原因就是,線程與資源是有關聯的。
比如,一個線程持有某個 lock ,我們在線程釋放 lock 前粗暴的停止了它的運行,那么可能導致其持有的 lock 永遠不能被釋放,等待在該 lock 上的其它線程也會永遠的阻塞在該處永遠無法繼續執行。
再比如,一個線程正在進行 IO 操作,占用着硬盤中的某個文件,我們粗暴的終止了該線程,則這個文件便得不到釋放,會一直處於被占用狀態。
進程是資源分配單位,線程是運行調度單位。但這並不意味着,線程可以不考慮資源的調度隨意的執行任務。恰恰相反的是,許多由線程發起申請得到的資源,只有發起申請的線程才能正確的釋放它們。因為即使其對同一進程中的其它線程可見,其它線程也並不知道釋放該資源的正確時機。
所以我們在終止線程時,不能采用類似 stop 這種簡單粗暴的方式。應該確保在終止線程時,線程並不是立即被終止,而是將控制權交給該線程,由該線程自行判斷應該做好哪些善后工作,做完這些善后工作后再停止。
JAVA 中的 interrupt 信號可以做到這一點,一個典型的安全終止線程的方式便是基於 interrupt 信號通知線程,以及通過 InterruptedException 異常回滾堆棧鈎出阻塞中的線程,使其停止。
關於 JVM 是如何響應 interrupt 信號並拋出 InterruptedException 的,前面的博客有非常全面的解釋:https://www.cnblogs.com/niuyourou/p/12392942.html。
我們來看一個 interrupt 終止線程的例子,一個典型的兩步終止:
public class ThreadInterrupt extends Thread{ @Override public void run(){ int i=0; try { while (!Thread.currentThread().isInterrupted()) { System.out.println("線程進行第 " + i + " 次工作,此時 inerrupt 標志位為:"+Thread.currentThread().isInterrupted()); Thread.sleep(100); i++; } //這里可以進行善后工作,比如釋放資源 System.out.println("線程接收到 interrupt 信號,離開工作區.此時 inerrupt 標志位為:"+Thread.currentThread().isInterrupted()); }catch (InterruptedException inE){ //這里可以進行善后工作,比如釋放資源 System.out.println("線程在 sleep 的過程中被 interrupted,此時 interrupt 標志位為:"+Thread.currentThread().isInterrupted()); } } }
線程每執行完一次任務,都會檢查一下是否有 interrupt 信號,如果有則進行善后工作並退出線程。
同時線程對 InterruptedException 進行了捕獲,如果在 sleep/wait/join 過程中收到 interrupt 信號,則回滾堆棧到 try 處,進入 catch 塊進行異常的處理,我們可以在catch 塊中進行善后工作。
這樣無論是運行狀態還是阻塞狀態(因為 sleep/wait/join 陷入的阻塞,因為我們只有通過這些方法可以使線程陷入阻塞。synchronized 等方法阻塞或喚醒線程是由 JVM 控制的,由 JVM 保證其正確性。)下的線程,都可以及時的響應我們的 interrupt 信號。
我們進行一下測試:
public class Test { public static void main(String[] args) throws InterruptedException{ Thread test1=new ThreadInterrupt(); test1.start(); Thread.sleep(1000); test1.interrupt(); } }
看一下效果,在 sleep 過程中接收到了 interrupt 信號:
在正常工作時接收到了 interrupt 信號:
我們可以看到,兩次被 interrupt 線程表現並不同。
sleep 情況下進入 catch 塊后,interrupt 狀態被重置為了 false,而正常狀態下被中斷時 interrupt 狀態未被重置,依然是true。
因為 interrupt 的本質是一個信號,正常情況下我們對一個信號進行了處理應該將其移出信號隊列(當然這里不是隊列),避免對其重復處理。
sleep 拋出異常前 JVM 幫助我們做到了這一點,我們在對信號進行處理時,也應該手動將其重置。
isInterrupted 只能獲取信號狀態,不會重置它。interrupted 可以返回信號的當前狀態,如果未 true,返回后會將其重置為 false 。我們可以使用 interrupted 方法代替 isInterrupted 方法:
public class ThreadInterrupt extends Thread{ @Override public void run(){ int i=0; try { while (!Thread.interrupted()) { System.out.println("線程進行第 " + i + " 次工作,此時 inerrupt 標志位為:"+Thread.currentThread().isInterrupted()); Thread.sleep(1); i++; } //這里可以進行善后工作,比如釋放資源 System.out.println("線程接收到 interrupt 信號,離開工作區.此時 inerrupt 標志位為:"+Thread.currentThread().isInterrupted()); }catch (InterruptedException inE){ //這里可以進行善后工作,比如釋放資源 System.out.println("線程在 sleep 的過程中被 interrupted,此時 interrupt 標志位為:"+Thread.currentThread().isInterrupted()); } } }
這樣再來看效果:
可以看到,在響應了中斷信號后,interrupted 方法幫我們將 interrupt 狀態重置為了 false。
第 50 次工作時,檢測到了 interrupt 信號,但線程還是將其執行完畢后才退出了工作區,這也證實了該方法的可靠性,使線程盡量安全的退出。