在Java中如何才能正在啟動一個線程Thread,實際上使用的是線程Thread的start()方法。但是如何停止一個正在運行的線程呢?線程Thread類提供了stop()方法,可是實際開發中幾乎沒有使用過線程Thread自己提供的stop()方法,因為stop()方法從JDK1.2開始就已經 Deprecated ,下面是JDK中對stop()方法的描述。
該方法具有固有的不安全性。用 Thread.stop 來終止線程將釋放它已經鎖定的所有監視器(作為沿堆棧向上傳播的未檢查 ThreadDeath 異常的一個自然後果)。如果以前受這些監視器保護的任何對象都處於一種不一致的狀態,則損壞的對象將對其他線程可見,這有可能導致任意的行為。stop的許多使用都應由只修改某些變量以指示目標線程應該停止運行的代碼來取代。目標線程應定期檢查該變量,並且如果該變量指示它要停止運行,則從其運行方法依次返回。如果目標線程等待很長時間(例如基於一個條件變量),則應使用 interrupt 方法來中斷該等待。有關更多信息,請參閱 為何不贊成使用 Thread.stop、Thread.suspend 和 Thread.resume?。
假設在同一時刻,銀行賬戶A去匯款到賬戶B,賬戶B也在查詢賬戶余額,如果ThreadA線程擁有了監視器,這些監視器負責保護某些臨界資源,這裏臨界資源就是匯款金額。然而在匯款過程中,ThreadA線程調用了threadA.stop()方法。結果導致監視器被釋放,那麽臨界資源匯款金額很可能出現不一致性,比方賬戶A減少了100,但是賬戶B卻沒有增加100,而線程ThreadB執行查詢賬戶B余額發現確實沒有增加,那麽這100元跑哪去了?
中斷停止線程
《並發編程實踐》一書中介紹, Java沒有提供任何機制來安全地終止線程。但它提供了中斷(interruption),這是一種協作機制,能夠使一個線程終止另一個線程當前的工作。 正如上面JDK中對stop方法的描述,應該使用interrupt方法來中斷線程。
java.lang.Thread類中提供了如下幾個方法處理中斷狀態:
方法名稱 方法描述 public static boolean interrupted 測試 當前線程是否已經中斷 。線程的 中斷狀態 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。public boolean isInterrupted ()
測試 線程是否已經中斷 。線程的 中斷狀態 不受該方法的影響。 public void interrupt ()中斷線程。
下面是一個使用interrupt相關方法的Demo。
public class MainTest { public static void main(String[] args) { Task task = new Task(); task.start(); task.interrupt(); system.out.println(task.isInterrupted()); System.out.println(Thread.interrupted()); } private static class Task extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + ":" + Thread.interrupted()); } } } } //true //0:true //false //1:false //2:false //3:false //4:false //...
通過上面介紹,我們知道stop()方法是過時的不再建議使用的,但是通過示例發現,Thread類中interrupt()方法並沒有停止一個正在運行的線程,這是為什麽呢?JDK中對interrupt()方法的描述也看不出所以然來,如下是JDK中的描述。
如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程中受阻,則其中斷狀態將被清除,它還將收到一個 InterruptedException。
如果該線程在可中斷的通道上的 I/O 操作中受阻,則該通道將被關閉,該線程的中斷狀態將被設置並且該線程將收到一個 ClosedByInterruptException。
如果該線程在一個 Selector 中受阻,則該線程的中斷狀態將被設置,它將立即從選擇操作返回,並可能帶有一個非零值,就好像調用了選擇器的 wakeup 方法一樣。
《並發編程實踐》是這樣描述interrupt()方法的, 調用interrupt()方法並不意味著立刻停止目標線程正在進行的工作,而只是傳遞了請求中斷的消息。 也即是說,它不會真正的中斷一個正在運行的線程,而只是發送了一下中斷請求(可以簡單的理解為就是設置了一個中斷標記位),然後由線程在下一個合適的時刻中斷自己。
每一個線程都有一個boolean類型的中斷狀態位。當中斷線程時,該線程的中斷標記位將會設置為true。interrupt()方法能夠中斷目標線程並設置中斷標記位,而isInterrupted()方法能返回目標線程的中斷狀態。靜態方法interrupted()會清除線程的中斷狀態,並返回它原來的值,這也是清除中斷狀態的唯一方法。
接下來再看一下示例的運行結果就容易理解了,當我們調用task.interrupt()方法時,實際上設置了中斷標記位true,所以接下來調用task.isInterrupted()查看線程的中斷狀態,直接返回true,在子線程中首次調用靜態方法Thread.interrupted()將會清除中斷標記,所以第二次調用時直接返回false,跟上面表格中介紹一致。
接著上面的討論,JDK描述線程Thread中的stop()方法是Deprecated,建議使用interrupt()方法替代,但是interrupt()方法並不會停止一個正在運行的線程,在對interrupt()方法的描述中也可以知道,它僅僅是設置了一個中斷標記。我們可以在上面例子中每次輸出時使用isInterrupted()方法判斷一下線程是否已經中斷,這樣如果已經設置了中斷標記直接跳出循環即可。
public class MainTest { public static void main(String[] args) { Task task = new Task(); task.start(); task.interrupt(); } private static class Task extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { if(Thread.currentThread().isInterrupted()){ System.out.println("interrupted"); break; }else{ System.out.println(i + ":" + Thread.interrupted()); } } } } }
通過這種實現方式,線程調用interrupt()方法後確實立刻就會退出循環了,可是問題到了這裏還沒有結束。JDK中描述了有關阻塞庫方法,例如Thread類中的sleep()、join()以及Object類中wait()方法,它們都會檢查何時中斷,並且在中斷時提前返回,而且在相應中斷時執行如下操作: 清除中斷狀態,拋出InterruptedException。 下面是JDK對sleep()方法的描述。
/** * Causes the currently executing thread to sleep (temporarily cease * execution) for the specified number of milliseconds, subject to * the precision and accuracy of system timers and schedulers. The thread * does not lose ownership of any monitors. * * @param millis * the length of time to sleep in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ public static native void sleep(long millis) throws InterruptedException;
在for循環中將線程執行以下睡眠,每格100ms輸出一個數值。
public class MainTest { public static void main(String[] args) { Task task = new Task(); task.start(); task.interrupt(); } private static class Task extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if(Thread.currentThread().isInterrupted()){ System.out.println("interrupted"); break; }else{ System.out.println(i + ":" + Thread.interrupted()); } } } } } //java.lang.InterruptedException: sleep interrupted // at java.lang.Thread.sleep(Native Method) // at com.sunny.demo.MainTest$Task.run(MainTest.java:18) //0:false //1:false //2:false //3:false //...
運行Demo發現線程又不能夠停止了,因為sleep()方法會響應中斷,然後清除中斷標記,拋出異常,註意下面再對中斷標記進行判斷就會出現Thread.currentThread().isInterrupted()為false,這樣造成線程還會繼續執行下去。由於阻塞庫方法會中斷線程,拋出異常,所以下面的代碼可以說是模板方法:
public void run() {
try {
// ① 調用阻塞方法
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ② 恢復被中斷的狀態
}
}
當阻塞方法響應中斷後再次調用線程的interrupt()方法恢復中斷,這樣問題似乎得到了解決。
在實際開發過程中,很多業務是不可以直接將異常捕獲直接就這樣簡單處理掉的,一般情況下有兩種策略可用於處理InterruptedException。
- 恢復中斷狀態,這樣可以讓調用棧中的上層代碼能夠對其進行處理;
- 傳遞異常(可能在執行某個特定的操作之後),從而使你的方法稱為可中斷的阻塞方法。
使用interrupt()確實可以中斷並停止線程,可是實際開發中一般都會重新定義一個方法進行線程的停止操作,在多線程開發中,卻很難知道寫的代碼將在哪個線程中運行,這也是在平常開發過程中很難直接看到一個停止或者取消的方法中有直接使用interrupt()方法。再者,調用interrupt()方法時,需要處理InterruptedException異常恢復中斷狀態,可是不是所有的阻塞方法都能響應中斷,例如一般IO操作阻並不會拋出InterruptedException。
那麽如何才能保證線程真正可靠的停止呢?
在線程同步的時候我們有一個叫”二次惰性檢測”(double check),能在提高效率的基礎上又確保線程真正中同步控制中。
那麽我把線程正確退出的方法稱為”雙重安全退出”,即不以isInterrupted()為循環條件,而以一個標記作為循環條件。
共享標誌停止線程
在文章開始部分 為何不贊成使用 Thread.stop、Thread.suspend 和 Thread.resume?。 介紹了使用一個volatile修飾的變量作為線程停止的一個標記,並在在停止的方法中調用interrupt()方法。
private volatile Thread blinker; public void stop() { Thread moribund = waiter; waiter = null; moribund.interrupt(); } public void run() { Thread thisThread = Thread.currentThread(); while (blinker == thisThread) { try { thisThread.sleep(interval); } catch (InterruptedException e) { } repaint(); } }
在這個示例中線程的停止標記是一個volatile修飾的線程,而實際開發中一般都是volatile修飾的Boolean類型或者整形的變量。JDK中自帶cancel()方法的類FutureTask中使用的就是一個volatile修飾的變量state,在判斷線程是否停止時,而且在cancel方法中將線程停止時使用了interrup()方法,代碼如下:
public boolean isCancelled() { return state >= CANCELLED; } public boolean cancel(boolean mayInterruptIfRunning) { if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; try { // in case call to interrupt throws exception if (mayInterruptIfRunning) { try { Thread t = runner; if (t != null) t.interrupt(); } finally { // final state UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } } } finally { finishCompletion(); } return true; }
在線程池ThreadPoolExecutor類中,用於判斷線程池是否已經關閉的isShutdown方法中也是對共享變量進行的判斷,ctl是一個AtomicInteger原子整形變量。而且在線程池類中shutdown方法中同樣使用了interrupt()方法,shutdown()方法的源碼這裏就不再貼出來了。
public boolean isShutdown() { return ! isRunning(ctl.get()); } private static boolean isRunning(int c) { return c < SHUTDOWN; }
小結
通過上面簡單的介紹可以知道如何要停止一個線程,最好的方式就是中斷+條件變量雙重安全退出策略。由於Java語言沒有提供任何機制來安全的退出線程,它只提供了中斷機制,這是一種協作機制,可以讓一個線程停止另一個線程的工作。所以在Java語言開發中想要使一個線程安全、快速、可靠地停止下來,並不是一件容易的事,在多線程開發中更多的建議是使用JDK中提供的線程池類,這樣不僅使用方便,而且不會因為頻繁創建和銷毀線程對系統產生過大的開銷。
參考資料
Why are Thread.stop, Thread.suspend and Thread.resume Deprecated ?
Thread的中斷機制(interrupt)
理解java線程的中斷(interrupt)
詳細分析Java中斷機制
如何停止一個正在運行的java線程
Tags: 線程 賬戶 方法 監視器 Thread stop
文章來源: