簡介
工作中我們經常會用到線程,一般情況下我們讓線程執行就完事了,那麼你們有沒有想過如何去終止一個正在運行的線程呢?
今天帶大家一起來看看。
Thread.stop被禁用之謎
問道怎麼終止一個線程,可能大多數人都知道可以調用Thread.stop方法。
但是這個方法從jdk1.2之後就不推薦使用了,爲什麼不推薦使用呢?
我們先來看下這個方法的定義:
@Deprecated(since="1.2")
public final void stop() {
@SuppressWarnings("removal")
SecurityManager security = System.getSecurityManager();
if (security != null) {
checkAccess();
if (this != Thread.currentThread()) {
security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
}
}
// A zero status value corresponds to "NEW", it can't change to
// not-NEW because we hold the lock.
if (threadStatus != 0) {
resume(); // Wake up thread if it was suspended; no-op otherwise
}
// The VM can handle all thread states
stop0(new ThreadDeath());
}
從代碼我們可以看出,stop這個方法首先檢測有沒有線程訪問的權限。如果有權限的話,來判斷當前的線程是否是剛剛創建的線程,如果不是剛剛創建的,那麼就調用resume方法來解除線程的暫停狀態。
最後調用stop0方法來結束線程。
其中resume和stop0是兩個native的方法,具體的實現這裏就不講了。
看起來stop方法很合理,沒有什麼問題。那麼爲什麼說這個方法是不安全的呢?
接下來我們來看一個例子。
我們創建一個NumberCounter的類,這個類有一個increaseNumber的安全方法,用來對number加一:
public class NumberCounter {
//要保存的數字
private volatile int number=0;
//數字計數器的邏輯是否完整
private volatile boolean flag = false;
public synchronized int increaseNumber() throws InterruptedException {
if(flag){
//邏輯不完整
throw new RuntimeException("邏輯不完整,數字計數器未執行完畢");
}
//開始執行邏輯
flag = true;
//do something
Thread.sleep(5000);
number++;
//執行完畢
flag=false;
return number;
}
}
事實上,在實際工作中這樣的方法可能需要執行比較久的時間,所以這裏我們通過調用Thread.sleep來模擬這個耗時操作。
這裏我們還有一個flag參數,來標誌這個increaseNumber方法是否成功執行完畢。
好了,接下來我們在一個線程中調用這個類的方法,看看會發生什麼:
public static void main(String[] args) throws InterruptedException {
NumberCounter numberCounter= new NumberCounter();
Thread thread = new Thread(()->{
while (true){
try {
numberCounter.increaseNumber();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(3000);
thread.stop();
numberCounter.increaseNumber();
}
這裏,我們創建了一個線程,等這個線程運行3秒鐘之後,直接調用thread.stop方法,結果我們發現出現了下面的異常:
Exception in thread "main" java.lang.RuntimeException: 邏輯不完整,數字計數器未執行完畢
at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:12)
at com.flydean.Main.main(Main.java:18)
這是因爲thread.stop方法直接終止了線程的運行,導致mberCounter.increaseNumber未執行完畢。
但是這個未執行完畢的狀態是隱藏的,如果使用thread.stop方法來終止線程,很有可能導致未知的結果。
所以,我們說thread.stop是不安全的。
怎麼才能安全?
那麼,如果不調用thread.stop方法,怎麼才能安全的終止線程呢?
所謂安全,那就是需要讓線程裏面的邏輯執行完畢,而不是執行一半。
爲了實現這個效果,Thread爲我們提供了三個比較類似的方法,他們分別是interrupt、interrupted和isInterrupted。
interrupt是給線程設置中斷標誌;interrupted是檢測中斷並清除中斷狀態;isInterrupted只檢測中斷。還有重要的一點就是interrupted是類方法,作用於當前線程,interrupt和isInterrupted作用於此線程,即代碼中調用此方法的實例所代表的線程。
interrupt就是中斷的方法,它的工作流程如下:
如果當前線程實例在調用Object類的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)方法,或者在該實例中調用了Thread.sleep(long)或Thread.sleep(long,int)方法,並且正在阻塞狀態中時,則其中斷狀態將被清除,並將收到InterruptedException。
如果此線程在InterruptibleChannel上的I/O操作中處於被阻塞狀態,則該channel將被關閉,該線程的中斷狀態將被設置爲true,並且該線程將收到java.nio.channels.ClosedByInterruptException異常。
如果此線程在java.nio.channels.Selector中處於被被阻塞狀態,則將設置該線程的中斷狀態爲true,並且它將立即從select操作中返回。
如果上面的情況都不成立,則設置中斷狀態爲true。
在上面的例子中,NumberCounter的increaseNumber方法中,我們調用了Thread.sleep方法,所以如果在這個時候,調用了thread的interrupt方法,線程就會拋出一個InterruptedException異常。
我們把上面調用的例子改成下面這樣:
public static void main(String[] args) throws InterruptedException {
NumberCounter numberCounter = new NumberCounter();
Thread thread = new Thread(() -> {
while (true) {
try {
numberCounter.increaseNumber();
} catch (InterruptedException e) {
System.out.println("捕獲InterruptedException");
throw new RuntimeException(e);
}
}
});
thread.start();
Thread.sleep(500);
thread.interrupt();
numberCounter.increaseNumber();
}
運行之後再試一次:
Exception in thread "main" Exception in thread "Thread-0" java.lang.RuntimeException: 邏輯不完整,數字計數器未執行完畢
at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:12)
at com.flydean.Main2.main(Main2.java:21)
java.lang.RuntimeException: java.lang.thread.interrupt: sleep interrupted
at com.flydean.Main2.lambda$main$0(Main2.java:13)
at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:17)
at com.flydean.Main2.lambda$main$0(Main2.java:10)
... 1 more
捕獲InterruptedException
可以看到,我們捕獲到了這個InterruptedException,並且得知具體的原因是sleep interrupted。
捕獲異常之後的處理
從上面的分析可以得知,thread.stop跟thread.interrupt的表現機制是不一樣的。thread.stop屬於悄悄終止,我們程序不知道,所以會導致數據不一致,從而產生一些未知的異常。
而thread.interrupt會顯示的拋出InterruptedException,當我們捕捉到這個異常的時候,我們就知道線程裏面的邏輯在執行的過程中受到了外部作用的干擾,那麼我們就可以執行一些數據恢復或者數據校驗的動作。
在上面的代碼中,我們是捕獲到了這個異常,打印出異常日誌,然後向上拋出一個RuntimeException。
正常情況下我們是需要在捕獲異常之後,進行一些處理。
那麼自己處理完這個異常之後,是不是就完美了呢?
答案是否定的。
因爲如果我們自己處理了這個InterruptedException, 那麼程序中其他部分如果有依賴這個InterruptedException的話,就可能會出現數據不一致的情況。
所以我們在自己處理完InterruptedException之後,還需要再次拋出這個異常。
怎麼拋出InterruptedException異常呢?
有兩種方式,第一種就是在調用Thread.interrupted()清除了中斷標誌之後立即拋出:
if (Thread.interrupted()) // Clears interrupted status!
throw new InterruptedException();
還有一種方式就是,在捕獲異常之後,調用Thread.currentThread().interrupt()再次中斷線程。
public void run () {
try {
while (true) {
// do stuff
}
}catch (InterruptedException e) {
LOGGER.log(Level.WARN, "Interrupted!", e);
// Restore interrupted state...
Thread.currentThread().interrupt();
}
}
這兩種方式都能達到預想的效果。
總結
線程不能調用stop來終止主要是因爲不會拋出異常,從而導致一些安全和數據不一致的問題。所以,最好的方式就是調用interrupt方法來處理。
本文的例子https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/how-to-stop-thread
更多文章請看 www.flydean.com