三種:已廢棄的Thread.stop()、迷惑的thread.interrupt系列、最佳實踐Shared Variable。
@Deprecated |
如上是Hotspot JDK 7中的java.lang.Thread.stop()的代碼,學習一下它的doc:
該方法天生是不安全的。使用thread.stop()停止一個線程,導致釋放(解鎖)所有該線程已經鎖定的監視器(因沿堆棧向上傳播的未檢查異常ThreadDeath而解鎖)。如果之前受這些監視器保護的任何對象處於不一致狀態,則不一致狀態的對象(受損對象)將對其他線程可見,這可能導致任意的行為。
是不是差點被這段話繞暈,俗點說:目標線程可能持有一個監視器,假設這個監視器控制着某兩個值之間的邏輯關系,如var1必須小於var2,某一時刻var1等於var2,本來應該受保護的邏輯關系,不幸的是此時恰好收到一個stop命令,產生一個ThreadDeath錯誤,監視器被解鎖。這就導致邏輯錯誤,當然這種情況也可能不會發生,是不可預料的。注意:ThreadDeath是何方神聖?是個java.lang.Error,不是java.lang.Exception。
public class ThreadDeath extends Error { |
thread.stop()方法的許多應用應該由“只修改某些變量以指示目標線程應該停止”的代碼取代。目標線程應周期性的檢查該變量,當發現該變量指示其要停止運行,則退出run方法。如果目標線程等待很長時間,則應該使用interrupt方法中斷該等待。
其實這里已經暗示停止一個線程的最佳方法:條件變量 或 條件變量+中斷。
更多請查看:
Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?
上文請參考我的翻譯xxx。
其它關於stop方法的doc:
- 該方法強迫停止一個線程,並拋出一個新創建的ThreadDeath對象作為異常。
- 停止一個尚未啟動的線程是允許的,如果稍后啟動該線程,它會立即終止。
- 通常不應試圖捕獲ThreadDeath,除非它必須執行某些異常的清除操作。如果catch子句捕獲了一個ThreadDeath對象,則必須重新拋出該對象,這樣該線程才會真正終止。
小結:
Thread.stop()不安全,已不再建議使用。
令人迷惑的thread.interrupt()
Thread類中有三個方法會令新手迷惑,他們是:
public void Thread.interrupt() // 無返回值 |
如果按照近幾年流行的重構,代碼整潔之道,程序員修煉之道等書的觀點,這幾個方法的命名相對於其實現的功能來說,不夠直觀明確,極易令人混淆,是低級程序猿的代碼。逐個分析:
public void interrupt() { |
中斷本線程。無返回值。具體作用分以下幾種情況:
- 如果該線程正阻塞於Object類的wait()、wait(long)、wait(long, int)方法,或者Thread類的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法,則該線程的中斷狀態將被清除,並收到一個java.lang.InterruptedException。
- 如果該線程正阻塞於interruptible channel上的I/O操作,則該通道將被關閉,同時該線程的中斷狀態被設置,並收到一個java.nio.channels.ClosedByInterruptException。
- 如果該線程正阻塞於一個java.nio.channels.Selector操作,則該線程的中斷狀態被設置,它將立即從選擇操作返回,並可能帶有一個非零值,就好像調用java.nio.channels.Selector.wakeup()方法一樣。
- 如果上述條件都不成立,則該線程的中斷狀態將被設置。
小結:第一種情況最為特殊,阻塞於wait/join/sleep的線程,中斷狀態會被清除掉,同時收到著名的InterruptedException;而其他情況中斷狀態都被設置,並不一定收到異常。
中斷一個不處於活動狀態的線程不會有任何作用。如果是其他線程在中斷該線程,則java.lang.Thread.checkAccess()方法就會被調用,這可能拋出java.lang.SecurityException。
public static boolean interrupted() { |
檢測當前線程是否已經中斷,是則返回true,否則false,並清除中斷狀態。換言之,如果該方法被連續調用兩次,第二次必將返回false,除非在第一次與第二次的瞬間線程再次被中斷。如果中斷調用時線程已經不處於活動狀態,則返回false。
public boolean isInterrupted() { |
檢測當前線程是否已經中斷,是則返回true,否則false。中斷狀態不受該方法的影響。如果中斷調用時線程已經不處於活動狀態,則返回false。
interrupted()與isInterrupted()的唯一區別是,前者會讀取並清除中斷狀態,后者僅讀取狀態。
在hotspot源碼中,兩者均通過調用的native方法isInterrupted(boolean)來實現,區別是參數值ClearInterrupted不同。
private native boolean isInterrupted(boolean ClearInterrupted); |
經過上面的分析,三者之間的區別已經很明確,來看一個具體案例,是我在工作中看到某位架構師的代碼,只給出最簡單的概要結構:
public void run() { |
我最初被這段代碼直接繞暈,用thread.isInterrupted()方法作為循環中止條件可以嗎?
根據上文的分析,當該方法阻塞於wait/join/sleep時,中斷狀態會被清除掉,同時收到InterruptedException,也就是接收到的值為false。上述代碼中,當sleep之后的調用otherDomain.xxx(),otherDomain中的代碼包含wait/join/sleep並且InterruptedException被catch掉的時候,線程無法正確的中斷。
因此,在編寫多線程代碼的時候,任何時候捕獲到InterruptedException,要么繼續上拋,要么重置中斷狀態,這是最安全的做法,參考『Java Concurrency in Practice』。凡事沒有絕對,如果你可以確保一定沒有這種情況發生,這個代碼也是可以的。
下段內容引自:『Java並發編程實戰』 第5章 基礎構建模塊 5.4 阻塞方法與中斷方法 p77
當某個方法拋出InterruptedException時,表示該方法是一個阻塞方法。當在代碼中調用一個將拋出InterruptedException異常的方法時,你自己的方法也就變成了一個阻塞方法,並且必須要處理對中斷的相應。對於庫代碼來說,有兩種選擇:
- 傳遞InterruptedException。這是最明智的策略,將異常傳遞給方法的調用者。
- 恢復中斷。在不能上拋的情況下,如Runnable方法,必須捕獲InterruptedException,並通過當前線程的interrupt()方法恢復中斷狀態,這樣在調用棧中更高層的代碼將看到引發了一個中斷。如下代碼是模板:
public void run() { |
最后再強調一遍,②處的 Thread.currentThread().interrupt() 非常非常重要。
最佳實踐:Shared Variable
不記得哪本書上曾曰過,最佳實踐是個爛詞。在這里這個詞最能表達意思,停止一個線程最好的做法就是利用共享的條件變量。
對於本問題,我認為准確的說法是:停止一個線程的最佳方法是讓它執行完畢,沒有辦法立即停止一個線程,但你可以控制何時或什么條件下讓他執行完畢。
通過條件變量控制線程的執行,線程內部檢查變量狀態,外部改變變量值可控制停止執行。為保證線程間的即時通信,需要使用使用volatile關鍵字或鎖,確保讀線程與寫線程間變量狀態一致。下面給一個最佳模板:
/** |
當④處的代碼阻塞於wait()或sleep()時,線程不能立刻檢測到條件變量。因此②處的代碼最好同時調用interrupt()方法。
小結:
How to Stop a Thread or a Task ? 詳細討論了如何停止一個線程, 總結起來有三點:
- 使用violate boolean變量來標識線程是否停止。
- 停止線程時,需要調用停止線程的interrupt()方法,因為線程有可能在wait()或sleep(), 提高停止線程的即時性。
- 對於blocking IO的處理,盡量使用InterruptibleChannel來代替blocking IO。
總結:
要使任務和線程能安全、快速、可靠地停止下來,並不是一件容易的事。Java沒有提供任何機制來安全地終止線程。但它提供了中斷(Interruption),這是一種協作機制,能夠使一個線程終止另一個線程的的工作。—— 『Java並發編程實戰』 第7章 取消與關閉 p111
中斷是一種協作機制。一個線程不能強制其它線程停止正在執行的操作而去執行其它的操作。當線程A中斷B時,A僅僅是要求B在執行到某個可以暫停的地方停止正在執行的操作——前提是如果線程B願意停下來。—— 『Java並發編程實戰』 第5章 基礎構建模塊 p77
總之,中斷只是一種協作機制,需要被中斷的線程自己處理中斷。停止一個線程最佳實踐是 中斷 + 條件變量。