1. 程式人生 > 其它 >執行緒中斷的方式及vilotile不合適的場景

執行緒中斷的方式及vilotile不合適的場景

原理

通常情況下,我們不會手動停止一個執行緒,而是允許執行緒執行到結束,然後讓它自然停止。但是依然會有許多特殊的情況需要我們提前停止執行緒,比如:使用者突然關閉程式,或程式執行出錯重啟等。

在這種情況下,即將停止的執行緒在很多業務場景下仍然很有價值。尤其是我們想寫一個健壯性很好,能夠安全應對各種場景的程式時,正確停止執行緒就顯得格外重要。但是Java 並沒有提供簡單易用,能夠直接安全停止執行緒的能力。

為什麼不強制停止?而是通知、協作

最正確的停止執行緒的方式是使用interrupt。
interrupt的作用:通知被停止執行緒的作用
為什麼只是起到了通知作用:
而對於被停止的執行緒而言,它擁有完全的自主權

,它既可以選擇立即停止,也可以選擇一段時間後停止,也可以選擇壓根不停

為什麼 Java 不提供強制停止執行緒的能力呢

  • 希望程式能相互通知、協作的管理執行緒

因為如果不瞭解對方正在做的工作,貿然強制停止執行緒就可能會造成一些安全的問題,為了避免造成問題就需要給對方一定的時間來整理收尾工作。比如:執行緒正在寫入一個檔案,這時收到終止訊號,它就需要根據自身業務判斷,是選擇立即停止,還是將整個檔案寫入成功後停止,而如果選擇立即停止就可能造成資料不完整,不管是中斷命令發起者,還是接收者都不希望資料出現問題。

如何用interrupt停止執行緒

while (!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
}

如何用程式碼實現停止執行緒的邏輯。

我們一旦呼叫某個執行緒的 interrupt() 之後,這個執行緒的中斷標記位就會被設定成 true。每個執行緒都有這樣的標記位,當執行緒執行時,應該定期檢查這個標記位,如果標記位被設定成 true,就說明有程式想終止該執行緒。回到原始碼,可以看到在 while 迴圈體判斷語句中,首先通過 Thread.currentThread().isInterrupt() 判斷執行緒是否被中斷,隨後檢查是否還有工作要做。&& 邏輯表示只有當兩個判斷條件同時滿足的情況下,才會去執行下面的工作。

public class StopThread implements Runnable {
    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count=" + count++);
        }
    }

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(30);
        thread.interrupt();
    }
}



在 StopThread 類的 run() 方法中,首先判斷執行緒是否被中斷,然後判斷 count 值是否小於 1000。這個執行緒的工作內容很簡單,就是列印 0~999 的數字,每列印一個數字 count 值加 1,可以看到,執行緒會在每次迴圈開始之前,檢查是否被中斷了。接下來在 main 函式中會啟動該執行緒,然後休眠 5 毫秒後立刻中斷執行緒,該執行緒會檢測到中斷訊號,於是在還沒列印完1000個數的時候就會停下來,這種就屬於通過 interrupt 正確停止執行緒的情況。

sleep 期間能否感受到中斷

public class StopRunningThread {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {
            int num = 1;
            try {
                while (!Thread.currentThread().isInterrupted() && num <= 1000) {
                    System.out.println(num++);
                    Thread.sleep(100000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread.start();
        Thread.sleep(100);
        thread.interrupt();
    }
}


結果說明 如果 sleep、wait 等可以讓執行緒進入阻塞的方法使執行緒休眠了,而處於休眠中的執行緒被中斷,那麼執行緒是可以感受到中斷訊號的,並且會丟擲一個 InterruptedException 異常,同時清除中斷訊號,將中斷標記位設定成 false。這樣一來就不用擔心長時間休眠中執行緒感受不到中斷了,因為即便執行緒還在休眠,仍然能夠響應中斷通知,並丟擲異常。

執行緒中斷的兩種處理方式

  • 方法丟擲InterruptedException
  • try/catch catch再次中斷
private void reInterrupt() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        e.printStackTrace();
    }
}

為什麼用 volatile 標記位的停止方法是錯誤的

錯誤的停止方法

  1. stop():直接停止執行緒,執行緒沒有時間在停止前儲存自己的邏輯,會出現資料完整性問題
  2. susppend()和resume()組合:不會釋放鎖就開始進入休眠,持有鎖的情況下很容易出現死鎖問題,執行緒在resume()之前不會釋放
  3. volatile標記位
  • 正確停止方法
public class VolatileCanStop {
    private static boolean cancel = false;
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {
            int num = 0;
            while (!cancel && num < 10000) {
                if (num % 10 == 0) {
                    System.out.println("10的倍數:" + num);
                }
                num++;
                try {
                    Thread.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(100);
        cancel = true;
    }
}


上面程式碼中使用了Runnable匿名執行緒,然後在 run() 中進行 while 迴圈,在迴圈體中又進行了兩層判斷,首先判斷 canceled 變數的值,canceled 變數是一個被 volatile 修飾的初始值為 false 的布林值,當該值變為 true 時,while 跳出迴圈,while 的第二個判斷條件是 num 值小於1000000(一百萬),在while 迴圈體裡,只要是 10 的倍數就打印出來,然後 num++。

  • 錯誤停止方法
  1. 首先,聲明瞭一個生產者 Producer,通過 volatile 標記的初始值為 false 的布林值 canceled 來停止執行緒。而在 run() 方法中,while 的判斷語句是 num 是否小於 100000 及canceled 是否被標記。while 迴圈體中判斷 num 如果是 50 的倍數就放到storage 倉庫中,storage 是生產者與消費者之間進行通訊的儲存器,當 num 大於 100000 或被通知停止時,會跳出 while 迴圈並執行 finally 語句塊,告訴大家“生產者結束執行”。
class Producer implements Runnable {

    public volatile boolean canceled = false;
    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 50 == 0) {
                    storage.put(num);
                    System.out.println(num + "是50的倍數,被放到倉庫中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生產者結束執行");
        }
    }
}
  1. 而對於消費者 Consumer,它與生產者共用同一個倉庫 storage,並且在方法內通過 needMoreNums() 方法判斷是否需要繼續使用更多的數字,剛才生產者生產了一些 50 的倍數供消費者使用,消費者是否繼續使用數字的判斷條件是產生一個隨機數並與 0.97 進行比較,大於 0.97 就不再繼續使用數字。
class Consumer {
    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        if (Math.random() > 0.97) {
            return false;
        }
        return true;
    }
}

  1. main方法,下面來看下 main 函式,首先建立了生產者/消費者共用的倉庫BlockingQueue storage,倉庫容量是 8,並且建立生產者並將生產者放入執行緒後啟動執行緒,啟動後進行 500 毫秒的休眠,休眠時間保障生產者有足夠的時間把倉庫塞滿,而倉庫達到容量後就不會再繼續往裡塞,這時生產者會阻塞,500 毫秒後消費者也被創建出來,並判斷是否需要使用更多的數字,然後每次消費後休眠 100 毫秒,這樣的業務邏輯是有可能出現在實際生產中的。

當消費者不再需要資料,就會將 canceled 的標記位設定為 true,理論上此時生產者會跳出 while 迴圈,並列印輸出“生產者執行結束”。

public class VolatileCannotStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(8);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(500);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消費了");
            Thread.sleep(100);
        }
        System.out.println("消費者不需要更多資料了。");

        //一旦消費不需要更多資料了,我們應該讓生產者也停下來,但是實際情況卻停不下來
        producer.canceled = true;
        System.out.println(producer.canceled);
    }
}

然而結果卻不是我們想象的那樣,儘管已經把 canceled 設定成 true,但生產者仍然沒有停止,這是因為在這種情況下,生產者在執行 storage.put(num)時發生阻塞,在它被叫醒之前是沒有辦法進入下一次迴圈判斷canceled的值的,所以在這種情況下用 volatile 是沒有辦法讓生產者停下來的,相反如果用 interrupt 語句來中斷,即使生產者處於阻塞狀態,仍然能夠感受到中斷訊號,並做響應處理。

總結

  1. 中斷執行緒使用interrupt()
  2. 方法宣告InterruptException或者catch後Thread.currentThread().interrupted();
  3. 不要使用stop(),suspend() resume()組合被標記過時的方法
  4. 儘量避免使用volatile停止執行緒