引入
螞蟻金服面試題:如何停止、中斷一個運作中的線程?

如果直接回答 interrupt 那就爆炸了💥,答案絕對是錯誤的,這就是對底層運作機制的不了解,是以我們首先要搞清楚這幾個API的異同點
方法名 | 傳回值 | 作用描述 |
interrupt | void | 一個執行個體方法,僅僅是将置标志符為true,不會停止線程 |
interrupted | boolean(靜态方法) | 判斷此線程是否被中斷,并且清除目前中斷狀态,這個方法主要做了2件事: 1、傳回目前線程的中斷狀态 2、将目前線程的中斷狀态設定為false *這個方法有點不好了解,因為連續調用2次的結果可能不一樣 |
isInterrupted | boolean | 判斷目前線程是否被中斷(通過檢測中斷辨別位置) |
中斷協商機制
首先,一個線程不應該由其他線程來強制中斷或者停止,而是應該由線程自己自行停止
是以,Thread.stop、Thread.suspend、Thread.resume 都已經廢棄了
其次,在Java中沒有辦法立刻停止一條線程,然而停止線程卻是很重要的,如取消一個耗時的操作。是以,Java提供了一種用于停止線程的機制--中斷
中斷協商機制并不是立刻+馬上+now,stop一個線程
中斷隻是一種協商機制,Java并沒有給中斷增加任何文法,中斷的過程完全是需要程式員自行實作。若需要中斷一個線程,你需要手動調用該線程的Interrupt方法,該方法也僅僅是将線程對象中斷的辨別為設定為True;接着你需要自己寫代碼不斷的檢測目前線程的表示為,如果為True表示為中斷,為false表示為未中斷;
通過調用線程對象的interrupt方法将該線程的辨別位置為true,可以在别的線程中調用,也可以在自己的線程中調用
中斷Demo
volatile實作
通過volatile的可見性實作線程中斷
AtomicBoolean實作
通過原子類進行實作
Thread類API實作
通過interrupt進行實作
package com.gzczy.concurrent.thread.interrupted;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Description 停止線程三種方法
* @Author chenzhengyu
* @Date 2021-03-21 15:22
*/
public class STWThreadDemo {
private static volatile boolean flag = true;
private static AtomicBoolean atomicBoolean = new AtomicBoolean(true);
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
//demo1();
//demo2();
demo3();
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(5);
new Thread(()->{
//flag = false;
//atomicBoolean.compareAndSet(true,false);
t1.interrupt();
}).start();
}
/**
* 第一種:使用volatile關鍵字進行停止
*/
public static void demo1(){
while (flag){
System.out.println("正在運作");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 第二種:通過原子類
*/
public static void demo2(){
while (atomicBoolean.get()){
System.out.println("正在運作");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 第三種:通過interrupt方法進行打斷
*/
public static void demo3(){
while (!Thread.currentThread().isInterrupted()){
System.out.println("正在運作");
}
}
}
源碼分析
具體來說,當對一個線程,調用interrupt()時
- 如果線程處于正常活躍的狀态,那麼将會該線程的中斷辨別設定為true,也就僅此而已,被設定中斷辨別的線程會繼續運作,不受影響。是以調用調用interrupt()時,需要被調用的線程自己進行配合才行
- 如果線程處于阻塞狀态(如圖1代碼所示)sleep、wait、join等狀态,在别的線程中調用目前對象的interrupt()時,線程會立刻退出阻塞狀态,并且抛出InterruptedException異常
生産案例
故障代碼
在生産上,我們不能單純的以為Interrupt能夠中斷線程,這隻是一種協商機制,請看下面案例
package com.gzczy.concurrent.thread.interrupted;
import java.util.concurrent.TimeUnit;
/**
* @Description 線程中斷demo
* @Author chenzhengyu
* @Date 2021-03-21 15:49
*/
public class InterruptErrorDemo {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
CanNotStop();
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(3);
Thread t2 = new Thread(()->{
t1.interrupt();
},"t2");
t2.start();
}
/**
* 抛出異常後仍然 無法正常打斷
*/
public static void CanNotStop(){
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Running.....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運作結果
Running.....
Running.....
Running.....
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.gzczy.concurrent.thread.interrupted.InterruptErrorDemo.CanNotStop(InterruptErrorDemo.java:31)
at com.gzczy.concurrent.thread.interrupted.InterruptErrorDemo.lambda$main$0(InterruptErrorDemo.java:14)
at java.lang.Thread.run(Thread.java:748)
Running.....
Running.....
Running.....
Running.....
通過運作結果我們可以觀察到,運作完成就抛出了異常,然後就一直停不下來了。這種情況會導緻在生産實際情況下的系統卡頓,因為程式無法正常終止。在代碼中,我們翻開interrupt源碼可以發現這句話:當我們打斷正在睡眠的線程時候,我們會清除打斷标記,由于t2剛剛是發起了打斷請求,設定打斷辨別為true,但是因為有睡眠代碼塊,打斷時候會把打斷辨別清空,導緻無法進入break代碼塊,即便是異常也沒有停,是以我們需要将線程中斷标記位置改為true
正确代碼
我們在剛剛catch代碼塊再次打斷,複位标志位,使其能夠正确的中斷線程
package com.gzczy.concurrent.thread.interrupted;
import java.util.concurrent.TimeUnit;
/**
* @Description 線程中斷demo
* @Author chenzhengyu
* @Date 2021-03-21 15:49
*/
public class InterruptErrorDemo {
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(() -> {
CanNotStop();
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(3);
Thread t2 = new Thread(()->{
t1.interrupt();
},"t2");
t2.start();
}
/**
* 抛出異常後仍然 無法正常打斷(原因請點選進入API檢視,sleep wait join方法打斷會清除打斷辨別)
* 在異常部分需要再次打斷 重置标志位
*/
public static void CanNotStop(){
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Running.....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
//我們在剛剛catch代碼塊再次打斷,複位标志位,使其能夠正确的中斷線程
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
}
方法加深了解
判斷下列代碼輸出的結果
package com.gzczy.concurrent.thread.interrupted;
import lombok.extern.slf4j.Slf4j;
/**
* @Description 對比打斷方法中間
* @Author chenzhengyu
* @Date 2021-03-21 16:14
*/
@Slf4j(topic = "c.InterruptCompare")
public class InterruptCompare {
public static void main(String[] args) {
log.info("==>"+Thread.interrupted());
log.info("==>"+Thread.interrupted());
log.info("========================");
Thread.currentThread().interrupt();
log.info("========================");
//通過觀察結果 我們可以看到這裡顯示為true 因為剛剛打斷了,與此同時會将打斷狀态清空,下次判斷為false
log.info("==>"+Thread.interrupted());
// 此方法并不會重置辨別位置,隻是單純的顯示一下目前狀态
log.info("==>"+Thread.currentThread().isInterrupted());
log.info("==>"+Thread.interrupted());
}
}
結果
16:47:54.803 [main] c.InterruptCompare - ==>false
16:47:54.806 [main] c.InterruptCompare - ==>false
16:47:54.806 [main] c.InterruptCompare - ========================
16:47:54.806 [main] c.InterruptCompare - ========================
16:47:54.806 [main] c.InterruptCompare - ==>true
16:47:54.806 [main] c.InterruptCompare - ==>false
16:47:54.806 [main] c.InterruptCompare - ==>false
底層源碼分析
log.info("==>"+Thread.interrupted());
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
log.info("==>"+Thread.currentThread().isInterrupted());
public boolean isInterrupted() {
return isInterrupted(false);
}
通過方法觀察我們可以看到清晰的表達了”中斷狀态将會傳入的ClearInterrupted參數值确定是否需要重置“
由此可以得出結論:
靜态方法Interrupted将會清除中斷狀态(傳入參數的ClearInterrupted為true)
執行個體方法isInterrupted則不會(傳入參數的ClearInterrupted為false)