
在之前的文章中 i-code.online -《并發編程-執行緒基礎》我們介紹了執行緒的創建和終止,從原始碼的角度去理解了其中的細節,那么現在如果面試有人問你 “如何優雅的停止一個執行緒?”, 你該如何去回答尼 ?能不能完美的回答尼?
- 對于執行緒的停止,通常情況下我們是不會去手動去停止的,而是等待執行緒自然運行至結束停止,但是在我們實際開發中,會有很多情況中我們是需要提前去手動來停止執行緒,比如程式中出現例外錯誤,比如使用者關閉程式等情況中,在這些場景下如果不能很好地停止執行緒那么就會導致各種問題,所以正確的停止程式是非常的重要的,
強行停止執行緒會怎樣?
-
在我們平時的開發中我們很多時候都不會注意執行緒是否是健壯的,是否能優雅的停止,很多情況下都是貿然的強制停止正在運行的執行緒,這樣可能會造成一些安全問題,為了避免造成這種損失,我們應該給與執行緒適當的時間來處理完當前執行緒的收尾作業, 而不至于影響我們的業務,
-
對于 Java 而言,最正確的停止執行緒的方式是使用
interrupt,但interrupt僅僅起到通知被停止執行緒的作用,而對于被停止的執行緒而言,它擁有完全的自主權,它既可以選擇立即停止,也可以選擇一段時間后停止,也可以選擇壓根不停止,可能很多同學會疑惑,既然這樣那這個存在的意義有什么尼,其實對于Java而言,期望程式之間是能夠相互通知、協作的管理執行緒 -
比如我們有執行緒在進行
io操作時,當程式正在進行寫檔案奧做,這時候接收到終止執行緒的信號,那么它不會立馬停止,它會根據自身業務來判斷該如何處理,是將整個檔案寫入成功后在停止還是不停止等都取決于被通知執行緒的處理,如果這里立馬終止執行緒就可能造成資料的不完整性,這是我們業務所不希望的結果,
interrupt 停止執行緒
- 關于
interrupt的使用我們不在這里過多闡述,可以看 i-code.online -《并發編程-執行緒基礎》文中的介紹,其核心就是通過呼叫執行緒的isInterrupt()方法進而判斷中斷信號,當執行緒檢測到為true時則說明接收到終止信號,此時我們需要做相應的處理
- 我們撰寫一個簡單例子來看
Thread thread = new Thread(() -> {
while (true) {
//判斷當前執行緒是否中斷,
if (Thread.currentThread().isInterrupted()) {
System.out.println("執行緒1 接收到中斷資訊,中斷執行緒...中斷標記:" + Thread.currentThread().isInterrupted());
//跳出回圈,結束執行緒
break;
}
System.out.println(Thread.currentThread().getName() + "執行緒正在執行...");
}
}, "interrupt-1");
//啟動執行緒 1
thread.start();
//創建 interrupt-2 執行緒
new Thread(() -> {
int i = 0;
while (i <20){
System.out.println(Thread.currentThread().getName()+"執行緒正在執行...");
if (i == 8){
System.out.println("設定執行緒中斷...." );
//通知執行緒1 設定中斷通知
thread.interrupt();
}
i ++;
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"interrupt-2").start();
上述代碼相對比較簡單,我們創建了兩個執行緒,第一個執行緒我們其中做了中斷信號檢測,當接收到中斷請求則結束回圈,自然的終止執行緒,在執行緒二中,我們模擬當執行到 i==8 時通知執行緒一終止,這種情況下我們可以看到程式自然的進行的終止,
這里有個思考: 當處于
sleep時,執行緒能否感受到中斷信號?
- 對于這一特殊情況,我們可以將上述代碼稍微修改即可進行驗證,我們將執行緒1的代碼中加入
sleep同時讓睡眠時間加長,讓正好執行緒2通知時執行緒1還處于睡眠狀態,此時觀察是否能感受到中斷信號
//創建 interrupt-1 執行緒
Thread thread = new Thread(() -> {
while (true) {
//判斷當前執行緒是否中斷,
if (Thread.currentThread().isInterrupted()) {
System.out.println("執行緒1 接收到中斷資訊,中斷執行緒...中斷標記:" + Thread.currentThread().isInterrupted());
Thread.interrupted(); // //對執行緒進行復位,由 true 變成 false
System.out.println("經過 Thread.interrupted() 復位后,中斷標記:" + Thread.currentThread().isInterrupted());
//再次判斷是否中斷,如果是則退出執行緒
if (Thread.currentThread().isInterrupted()) {
break;
}
break;
}
System.out.println(Thread.currentThread().getName() + "執行緒正在執行...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "interrupt-1");
我們執行修改后的代碼,發現如果 sleep、wait 等可以讓執行緒進入阻塞的方法使執行緒休眠了,而處于休眠中的執行緒被中斷,那么執行緒是可以感受到中斷信號的,并且會拋出一個 InterruptedException 例外,同時清除中斷信號,將中斷標記位設定成 false,這樣一來就不用擔心長時間休眠中執行緒感受不到中斷了,因為即便執行緒還在休眠,仍然能夠回應中斷通知,并拋出例外,
對于執行緒的停止,最優雅的方式就是通過
interrupt的方式來實作,關于他的詳細文章看之前文章即可,如InterruptedException時,再次中斷設定,讓程式能后續繼續進行終止操作,不過對于interrupt實作執行緒的終止在實際開發中發現使用的并不是很多,很多都可能喜歡另一種方式,通過標記位,
用 volatile 標記位的停止方法
- 關于
volatile作為標記位的核心就是他的可見性特性,我們通過一個簡單代碼來看:
/**
* @ulr: i-code.online
* @author: zhoucx
* @time: 2020/9/25 14:45
*/
public class MarkThreadTest {
//定義標記為 使用 volatile 修飾
private static volatile boolean mark = false;
@Test
public void markTest(){
new Thread(() -> {
//判斷標記位來確定是否繼續進行
while (!mark){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒執行內容中...");
}
}).start();
System.out.println("這是主執行緒走起...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//10秒后將標記為設定 true 對執行緒可見,用volatile 修飾
mark = true;
System.out.println("標記位修改為:"+mark);
}
}
上面代碼也是我們之前文中的,這里不再闡述,就是一個設定標記,讓執行緒可見進而終止程式,這里我們需要討論的是,使用 volatile 是真的都是沒問題的,上述場景是沒問題,但是在一些特殊場景使用 volatile 時是存在問題的,這也是需要注意的!
volatile 修飾標記位不適用的場景
- 這里我們使用一個生產/消費的模式來實作一個
Demo
/**
* @url: i-code.online
* @author: zhoucx
* @time: 2020/10/12 10:46
*/
public class Producter implements Runnable {
//標記是否需要產生數字
public static volatile boolean mark = true;
BlockingQueue<Integer> numQueue;
public Producter(BlockingQueue numQueue){
this.numQueue = numQueue;
}
@Override
public void run() {
int num = 0;
try {
while (num < 100000 && mark){
//生產數字,加入到佇列中
if (num % 50 == 0 ){
System.out.println(num + " 是50的倍數,加入佇列");
numQueue.put(num);
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("生產者運行結束....");
}
}
}
首先,宣告了一個生產者 Producer,通過 volatile 標記的初始值為 true 的布林值 mark 來停止執行緒,而在 run() 方法中,while 的判斷陳述句是 num 是否小于 100000 及 mark 是否被標記,while 回圈體中判斷 num 如果是 50 的倍數就放到 numQueue 倉庫中,numQueue 是生產者與消費者之間進行通信的存盤器,當 num 大于 100000 或被通知停止時,會跳出 while 回圈并執行 finally 陳述句塊,告訴大家“生產者運行結束”
/**
* @url: i-code.online
* @author: zhoucx
* @time: 2020/10/12 11:03
*/
public class Consumer implements Runnable{
BlockingQueue numQueue;
public Consumer(BlockingQueue numQueue){
this.numQueue = numQueue;
}
@Override
public void run() {
try {
while (Math.random() < 0.97){
//進行消費
System.out.println(numQueue.take()+"被消費了...");;
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("消費者執行結束...");
Producter.mark = false;
System.out.println("Producter.mark = "+Producter.mark);
}
}
}
而對于消費者 Consumer,它與生產者共用同一個倉庫 numQueue,在 run() 方法中我們通過判斷亂數大小來確定是否要繼續消費,剛才生產者生產了一些 50 的倍數供消費者使用,消費者是否繼續使用數字的判斷條件是產生一個亂數并與 0.97 進行比較,大于 0.97 就不再繼續使用數字,
/**
* @url: i-code.online
* @author: zhoucx
* @time: 2020/10/12 11:08
*/
public class Mian {
public static void main(String[] args) {
BlockingQueue queue = new LinkedBlockingQueue(10);
Producter producter = new Producter(queue);
Consumer consumer = new Consumer(queue);
Thread thread = new Thread(producter,"producter-Thread");
thread.start();
new Thread(consumer,"COnsumer-Thread").start();
}
}
主函式中很簡單,創建一個 公共倉庫 queue 長度為10,然后傳遞給兩個執行緒,然后啟動兩個執行緒,當我們啟動后要注意,我們的消費時有睡眠 100 毫秒,那么這個公共倉庫必然會被生產者裝滿進入阻塞,等待消費,
當消費者不再需要資料,就會將 canceled 的標記位設定為 true,理論上此時生產者會跳出 while 回圈,并列印輸出“生產者運行結束”,
然而結果卻不是我們想象的那樣,盡管已經把 Producter.mark設定成 false,但生產者仍然沒有停止,這是因為在這種情況下,生產者在執行 numQueue.put(num) 時發生阻塞,在它被叫醒之前是沒有辦法進入下一次回圈判斷 Producter.mark的值的,所以在這種情況下用 volatile 是沒有辦法讓生產者停下來的,相反如果用 interrupt 陳述句來中斷,即使生產者處于阻塞狀態,仍然能夠感受到中斷信號,并做回應處理,
總結
通過上面的介紹我們知道了,執行緒終止的主要兩種方式,一種是 `interrupt` 一種是`volatile` ,兩種類似的地方都是通過標記來實作的,不過`interrupt` 是中斷信號傳遞,基于系統層次的,不受阻塞影響,而對于 `volatile` ,我們是利用其可見性而頂一個標記位標量,但是當出現阻塞等時無法進行及時的通知,
在我們平時的開發中,我們視情況而定,并不是說必須使用 `interrupt` ,在一般情況下都是可以使用 `volatile` 的,但是這需要我們精確的掌握其中的場景,
本文由AnonyStar 發布,可轉載但需宣告原文出處,
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關注微信公賬號 :云棲簡碼 獲取更多優質文章
更多文章關注筆者博客 :云棲簡碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/169628.html
標籤:Java
