如何正確停止線程
原理介紹
- 使用interrupt來通知,而不是強制停止
Java中停止線程的原則是什麼
- 在Java中,最好的停止線程的方式是使用中斷interrupt,但是
這僅僅是會通知到被終止的線程“你該停止運行了”,被終止的線程自身擁有決定權(決定是否、以及何時停止)
,這依賴於請求停止方和被停止方都遵守一種約定好的編碼規範。 - 任務和線程的啓動很容易。
在大多數時候,我們都會讓它們運行直到結束,或者讓它們自行停止
。然而有時候我們希望提前結束任務或線程或許是因爲用戶取消了操作或者服務需要被快速關閉,或者是運行超時或出錯了。 - 要使任務和線程能安全、快速、可靠地停止下來,並不是一件容易的事。Java沒有提供任何機制來安全地終止線程。但它提供了中斷(Interruption),
這是一種協作機制,能夠使一個線程終止另—個線程的當前工作
。 - 這種協作式的方法是必要的,
我們很少希望某個任務、線程或服務立即停止,因爲這種立即停止會使共享的數據結構處於不一致的狀態
。相反,在編寫任務和服務時可以使用種協作的方式當需要停止時它們首先會清除當前正在執行的工作然後再結束。這提供了更好的靈活性,因爲任務本身的代碼比發出取消請求的代碼更清楚如何執行清除工作。
- 生命週期結束(End- of-Lifecycle)的問題會使任務、服務以及程序的設計和實現等過程變得複雜,而這個在程序設計中非常重要的要素卻經常被忽略。一個在行爲良好的軟件與勉強運的軟件之間的最主要區別就是,
行爲良好的軟件能很完善地處理失敗、關閉和取消等過程。
- 接下來將給岀各種實現取消和中斷的機制,以及如何編寫任務和服務,使它們能對取消請求做出響應。
正確的停止方法:interrupt
我們分三種情況討論interrupt如何停止線程。
1.通常線程會在什麼情況下停止普通情況?
示例一
:run方法內沒有sleep或wait方法時,停止線程
public class RightWayStopThreadWithoutSleep implements Runnable {
@Override
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍數");
}
num++;
}
System.out.println("任務運行結束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(2000);
thread.interrupt();
}
}
打印結果:
...
279980000是10000的倍數
279990000是10000的倍數
280000000是10000的倍數
280010000是10000的倍數
任務運行結束了
雖然主線程調用了thread.interrupt()方法,但他只是把子線程標記爲中斷,代碼中還需要不停地去判斷是否被中斷,即:Thread.currentThread().isInterrupted()
2.線程可能被阻塞
示例二
:帶有sleep的中斷線程的寫法
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 300 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
打印結果:
0是100的倍數
100是100的倍數
200是100的倍數
300是100的倍數
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:21)
at java.lang.Thread.run(Thread.java:745)
Thread.sleep()阻塞過程中能夠檢測到中斷標記,而拋出中斷異常。
3.如果線程在每次迭代後都阻塞
示例三
:如果在執行過程中,每次循環都會調用sleep或wait等方法,那麼不需要每次迭代都檢查是否已中斷,sleep會響應中斷
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印結果:
0是100的倍數
100是100的倍數
200是100的倍數
300是100的倍數
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
at java.lang.Thread.run(Thread.java:745)
這個案例中我們不再需要檢查是否中斷,sleep阻塞時候會自動檢測中斷,即不再需要:Thread.currentThread().isInterrupted()
4.while內try/catch的問題
示例四
:如果while裏面放try/catch,會導致中斷失效
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍數");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印結果:
0是100的倍數
100是100的倍數
200是100的倍數
300是100的倍數
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.CantInterrupt.lambda$main$0(CantInterrupt.java:20)
at java.lang.Thread.run(Thread.java:745)
400是100的倍數
500是100的倍數
600是100的倍數
...
Thread.sleep()檢測到的中斷信號拋出的異常被捕獲了。
5.實際開發中的兩種最佳實踐
- 優先選擇:傳遞中斷
示例五
:最佳實踐:catch了InterruptedExcetion之後的優先選擇:在方法簽名中拋出異常 那麼在run()就會強制try/catch
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
//保存日誌、停止程序
System.out.println("保存日誌");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印結果:
go
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:27)
at com.hd.thread.stop.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:16)
at java.lang.Thread.run(Thread.java:745)
保存日誌
處理中斷的最好方法是什麼?
優先選擇在方法上拋出異常。用 throws Interrupted Exception標記你的方法,不採用try語句塊捕獲異常,以便於該異常可以傳遞到頂層,讓run方法可以捕獲這一異常,例如
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
由於run方法內無法拋出checked Exception(只能用 try catch),頂層方法必須處理該異常,避免了漏掉或者被吞掉的情況,增強了代碼的健壯性。
- 不想或無法傳遞:恢復中斷
示例六
:最佳實踐2:在catch子語句中調用Thread.currentThread().interrupt()來恢復設置中斷狀態,以便於在後續的執行中,依然能夠檢查到剛纔發生了中斷回到剛纔RightWayStopThreadInProd補上中斷,讓它跳出。
public class RightWayStopThreadInProd2 implements Runnable {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted,程序運行結束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印結果:
Interrupted,程序運行結束
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hd.thread.stop.RightWayStopThreadInProd2.reInterrupt(RightWayStopThreadInProd2.java:25)
at com.hd.thread.stop.RightWayStopThreadInProd2.run(RightWayStopThreadInProd2.java:19)
at java.lang.Thread.run(Thread.java:745)
如果不能拋出中斷,要怎麼做?
如果不想或無法傳遞 InterruptedEXception(例如用run方法的時候,就不讓該方法throws InterruptedEXception),那麼應該選擇在 catch子句中調用Thread.currentThread().interrupt()來恢復設置中斷狀態,以便於在後續的執行依然能夠檢查到剛纔發生了中斷。
看上面代碼,線程在sleep期間被中斷,並且由 catch捕獲到該中斷並重新設置了中斷狀態,以便於可以在下—個循環的時候檢測到中斷狀態,正常退出。
- 不應屏蔽中斷
6.響應中斷的方法總結列表
- Object.wait() / wait(long) / wait(long, int)
- Thread.sleep(long) / sleep(long, int)
- Thread.join() / join( long) / join(long, int)
- java.util.concurrent.BlockingQueue.take() / put (E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await
- java.util.concurrent.CyclicBarrier.await
- java.util.concurrent.Exchanger.exchange(v)
- java.nio.channels.InterruptibleChannel相關方法
- java.nio.channels.Selector的相關方法