問題
Java中提供了很多調度線程的方法,下面一篇文章《如何合理地調度多線程程序,常用的方法有哪些》介紹了其中一種控制線程的方法:如何等待一個線程結束。那麼如果不希望等待線程結束,而是根據問題的需要隨時都要中斷線程使其結束,這種對線程的控制方法該如何實現呢?
解決思路
首先必須先明確“中斷”這個概念的實際含義,這裡的中斷是指一個線程在其任務完成之前被強行停止,提前消亡的過程。查閱JDK的幫助文檔,可以找到這樣一個和中斷有關的方法:interrupt()。
它的語法格式如下所示:
public void interrupt()
該方法的功能是中斷一個線程的執行。但是,在實際使用當中發現,這個方法不一定能夠真地中斷一個正在運行的線程。下面通過一個例子來看一看使用interrput()方法中斷一個線程時所出現的結果。程序代碼如下所示:
// 例1 InterruptThreadDemo.Java
class MyThread extends Thread
{
public void run()
{
while(true) // 無限循環,並使線程每隔1秒輸出一次字符串
{
System.out.println(getName()+" is running");
try{
sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
}
}
}
class InterruptThreadDemo
{
public static void main(String[] args) throws InterruptedException
{
MyThread m=new MyThread(); // 創建線程對象m
System.out.println("Starting thread...");
m.start(); // 啟動線程m
Thread.sleep(2000); //主線程休眠2秒,使線程m一直得到執行
System.out.println("Interrupt thread...");
m.interrupt(); // 調用interrupt()方法中斷線程m
Thread.sleep(2000); // 主線程休眠2秒,觀察中斷後的結果
System.out.println("Stopping application..."); // 主線程結束
}
}
這個程序的本意是希望,當程序執行到m.interrupt()方法後,線程m將被中斷並進入消亡狀態。然而運行這個程序,屏幕裡顯示了出人意料的結果,如圖1所示。
通過對結果的分析,可以發現,用戶線程在調用了interrupt()方法之後並沒有被中斷,而是繼續執行,直到人為地按下Ctrl+C或者Pause鍵為止。這個例子說明一個事實,直接使用interrput()方法並不能中斷一個正在運行的線程。那麼用什麼樣的方法才能中斷一個正在運行的線程呢?
圖 1 對線程調用了interrupt()通過查閱JDK,有些讀者可能會看到Thread類中所提供的stop()方法。但是在這裡需要強調的是,雖然該方法確實能夠停止一個正在運行的線程,但是該方法是不安全的,因為有時使用它會導致嚴重的系統錯誤。例如一個線程正在等待關鍵的數據結構,並只完成了部分地改變,如果在這一時刻停止該線程,那麼數據結構將會停留在錯誤的狀態上。正因為如此,在Java後期的版本中,它將不復存在。因此,使用stop()方法來中斷一個線程是不合適的。
這時我們想到了使用共享變量的方式,通過一個共享信號變量來通知線程是否需要中斷,如果需要中斷,則停止正在執行的任務,否則讓任務繼續執行。這種方式是如何實現的呢?
具體步驟
在這種方式中,之所以引入共享變量,是因為該變量可以被多個執行相同任務的線程用來作為是否中斷的信號,通知中斷線程的執行。下面通過在程序中引入共享變量來改進前面例1,改進後的代碼如下所示:
// 例2 InterruptThreadDemo2.Java
class MyThread extends Thread
{
boolean stop = false; // 引入一個布爾型的共享變量stop
public void run()
{
while(!stop) // 通過判斷stop變量的值來確定是否繼續執行線程體
{
System.out.println(getName()+" is running");
try
{
sleep(1000);
}catch(InterruptedException e){
System.out.println(e.getMessage());
}
}
System.out.println("Thread is exiting...");
}
}
class InterruptThreadDemo2
{
public static void main(String[] args) throws InterruptedException
{
MyThread m=new MyThread();
System.out.println("Starting thread...");
m.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...");
m.stop=true; // 修改共享變量
Thread.sleep(3000); // 主線程休眠以觀察線程中斷後的情況
System.out.println("Stopping application...");
}
}
在使用共享變量來中斷一個線程的過程中,線程體通過循環來周期性的檢查這一變量的狀態。如果變量的狀態改變,說明程序發出了立即中斷該線程的請求,此時,循環體條件不再滿足,結束循環,進而結束線程的任務。程序執行的結果如圖2所示:
圖2 引入共享變量來中斷線程其中,主程序中的第二個Thread.sleep(3000);語句就是用來使程序不提早結束,以便觀察線程m的中斷情況。結果是一旦將共享變量stop設置為true,則中斷立即發生。
為了更加安全起見,通常需要將共享變量定義為volatile類型或者將對該共享變量的一切訪問封裝到同步的代碼或者同步方法中去。後者所提到的技術將在第4.5節中介紹。
在多線程的程序中,當出現有兩個或多個線程共享同一實例變量的情況時,每一個線程可以保持這個實例變量自己的私有副本,變量的實際備份在不同時間被更新。而問題就是變量的主備份總是需要反映它的當前狀態,此時反而使效率降低。為保證效率,只需要簡單地指定變量為volatile類型即可,它可以告訴編譯器必須總是使用volatile變量的主備份(或者至少總是保持任何私有的備份和最新的備份一樣,反之亦然)。同樣,對主變量的訪問必須同任何私有備份一樣,精確地順序執行。
如果需要一次中斷所有由同一線程類創建的線程,該怎樣實現呢?有些讀者可能馬上就想到了對每一個線程對象通過設置共享變量的方式來中斷線程。這種方法當然可以,那麼有沒有更好的方法呢?
此時只需將共享變量設置為static類型的即可。然後在主程序中當需要中斷所有同一個線程類創建的線程對象時,使用MyThread.stop=true;語句就可實現對所有同一個線程類創建的線程對象的中斷操作,而且效率明顯提高。讀者不妨試一試。
專家說明
通過本節介紹了如何中斷一個正在執行的線程,既不是用stop()方法,也不是用interrupt()方法,而是通過引入了共享變量的形式有效地解決了線程中斷的問題。其實這種方法有很多好處,它避免了一些無法想象的意外情況的發生,特別是將共享變量所訪問的一切代碼都封裝到同步方法中以後,安全性將更高。在本節中,還可以嘗試創建多個線程來檢驗這種中斷方式的好處。此外,還介紹了volatile類型說明符的作用,這更加有助於提高中斷線程的效率,值得提倡。
專家指點
本小節不僅要掌握如何使用共享變量的方法來中斷一個線程,還要明白為什麼使用其他方法來中斷線程就不安全。其實,在多線程的調度當中還會出現一個問題,那就是死鎖。死鎖的出現將導致線程間均無法向前推進,從而陷入尴尬的局面。因此,為減少出現死鎖的發生,Java 1.2以後的版本中已經不再使用Thread類的stop(),suspend(),resume()以及destroy()方法。特別是不安全的stop()方法,原因就是它會解除由線程獲取的所有鎖定,而且一旦對象處於一種不連貫的狀態,那麼其他線程就能在那種狀態下檢查和修改它們,結果導致很難再檢查出問題的真正所在。因此最好的方法就是,用一個標志來告訴線程什麼時候應該退出自己的run()方法,並中斷自己的執行。通過後面小節的學習將會更好的理解這個問題。
相關問題
如果一個線程由於等待某些事件的發生而被阻塞,又該如何實現該線程的中斷呢?比如當一個線程由於需要等候鍵盤輸入而被阻塞,處於不可運行狀態時,即使主程序中將該線程的共享變量設置為true,但該線程此時根本無法檢查循環標志,當然也就無法立即中斷。
其實,這種情況經常會發生,比如調用Thread.join()方法,或者Thread.sleep()方法,在網絡中調用ServerSocket.accept()方法,或者調用了DatagramSocket.receive()方法時,都有可能導致線程阻塞。即便這樣,仍然不要使用stop()方法,而是使用Thread提供的interrupt()方法,因為該方法雖然不會中斷一個正在運行的線程,但是它可以使一個被阻塞的線程拋出一個中斷異常,從而使線程提前結束阻塞狀態,退出堵塞代碼。
下面看一個例子來說明這個問題:
// 例3 InterruptThreadDemo3.Java
class MyThread extends Thread
{
volatile boolean stop = false;
public void run()
{
while(!stop)
{
System.out.println(getName()+" is running");
try
{
sleep(1000);
}catch(InterruptedException e){
System.out.println("week up from blcok...");
stop=true; // 在異常處理代碼中修改共享變量的狀態
}
}
System.out.println(getName()+" is exiting...");
}
}
class InterruptThreadDemo3
{
public static void main(String[] args) throws InterruptedException
{
MyThread m1=new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...:"+m1.getName());
m1.stop=true; // 設置共享變量為true
m1.interrupt(); // 阻塞時退出阻塞狀態
Thread.sleep(3000); // 主線程休眠3秒以便觀察線程m1的中斷情況
System.out.println("Stopping application...");
}
}
程序中如果線程m1發生了阻塞,那麼雖然執行了m1.stop=true;語句,但是stop的值並未改變。為了能夠中斷該線程,必須在異常處理語句中對共享變量的值進行重新設置,從而實現了在任何情況下都能夠中斷線程的目的。
一定要記住,m1.interrupt();語句只有當線程發生阻塞時才有效。它的作用就是拋出一個InterruptedException類的異常對象,使try…catch語句捕獲異常,並對其進行處理。請讀者仔細研究這個程序,以便能夠看出其中的巧妙之處。