1. 程式人生 > 程式設計 >淺析執行緒的正確停止

淺析執行緒的正確停止

如何正確停止執行緒

1. 講解原理

原理介紹:使用interrupt來通知,而不是強制。 Java中停止執行緒的原則是什麼?

在Java中,最好的停止執行緒的方式是使用中斷interrupt,但是這僅僅是會通知到被終止的執行緒“你該停止運行了”,被終止的執行緒自身擁有決定權(決定否、以及何時停止),這依賴於請求停止方和被停止方都遵守一種約定好的編碼規範。

任務和執行緒的啟動很容易。在大多數時候,我們都會讓它們執行直到結束,或者讓它們自行停止。然而,有時候我們希望提前結束任務或執行緒或許是因為使用者取消了操作,或者服務需要被快速關閉,或者是執行超時或出錯了。

要使任務和執行緒能安全、快速、可靠地停止下來並不是一件容易的事。Java沒有提供任何機制來安全地終止執行緒。但它提供了中斷(Interruption),這是一種協作機制

,能夠使一個執行緒終止另一個執行緒的當前工作。

這種協作式的方法是必要的,我們很少希望某個任務、執行緒或服務立即停止,因為這種立即停止會使共享的資料結構處於不一致的狀態。相反,在編寫任務和服務時可以使用一種協作的方式當需要停止時,它們首先會清除當前正在執行的工作然後再結束。這提供了更好的靈活性,因為任務本身的程式碼比發出取消請求的程式碼更清楚如何執行清除工作

生命週期結束(End- of-Lifecycle)的問題會使任務、服務以及程式的設計和實現等過程變得複雜,而這個在程式設計中非常重要的要素卻經常被忽略。一個在行為良好的軟體與勉強運的軟體之間的最主要區別就是行為良好的軟體能很完善地處理失敗、關閉和取消等過程。

本章將給出各種實現取消和中斷的機制,以及如何編寫任務和服務,使它們能對取消請求做出響應

2. 最佳實踐:如何正確停止執行緒

2.1 執行緒通常會在什麼情況下停止

1、run()方法執行完畢
2、 有異常出現,並且執行緒中沒有捕獲。
執行緒停止以後,所佔用的資源會被jvm回收。

2.2 正確的停止方法:interrupt

2.2.1 通常情況下如何停止

package stopthreads;

/**
 * 描述: run()方法內沒有sleep()和wait()方法時,停止執行緒。
 */

public class RightStopThreadWithoutSleep implements
Runnable
{ public static void main(String[] args) { Thread thread = new Thread(new RightStopThreadWithoutSleep()); thread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt(); } @Override public void run() { int num = 0; while (num <= Integer.MAX_VALUE / 2){ if (!Thread.currentThread().isInterrupted() && num % 10000 == 0) { System.out.println(num + "是10000的倍數"); } num++; } System.out.println("任務結束了。"); } } 複製程式碼

注意: thread.interrupt();無法強制的中斷執行緒,必須有要被中斷的執行緒的配合。
即:需要在子執行緒中加上如下程式碼:!Thread.currentThread().isInterrupted()

2.2.2 執行緒可能被阻塞如何停止

package stopthreads;

import static java.lang.Thread.*;

public class RightStopThreadWithSleep {
    
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            while (num <= 300){
                if (num % 100 == 0 && !currentThread().isInterrupted()){
                    System.out.println(num + "是100的整數倍");
                }
                num++;
            }
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

}

複製程式碼

結果如下:

2.2.3 如果執行緒在每次迭代後都阻塞

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

/**
 * 描述:如果在執行過程中,每次迴圈都會呼叫sleep()或wait()等方法,那麼...
 */
public class rightStopThreadWithSleepEveryLoop {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            try {
                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整數倍");
                    }
                    num++;
                    sleep(10);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

複製程式碼

while內的try-catch問題:

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;

                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整數倍");
                    }
                    num++;
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

複製程式碼

改變try-catch位置,結果完全不一樣:

注意: 即便新增上:&& !currentThread().isInterrupted()依然沒有效果!
原因: Thread類在設計時,sleep()呼叫中斷後,interrupt標記物會自動清除!

2.3 實際開發中的兩種最佳實踐

2.3.1 最佳實踐一:優先選擇:傳遞中斷(方法的簽名上丟擲異常)

我們先加一個小插曲: 錯誤地處理異常, 在被呼叫的方法中,直接把InterruptException catch掉,這樣做相當於在低層的方法中就把異常給吞了,導致上層的呼叫無法感知到有異常。
正確做法應該是:丟擲異常, 而異常的真正處理,應該叫個呼叫它的那個函式。
錯誤程式碼如下:

package stopthreads;

/**
 * 描述:  catch了InterruptionException之後的優先選擇:在方法簽名中丟擲異常。
 * 那麼,在run()中就會強制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() {

        /**
         * 錯誤做法:這樣做相當於就把異常給吞了,導致上層的呼叫無法感知到有異常
         * 正確做法應該是,丟擲異常,而異常的真正處理,應該叫個呼叫它的那個函式。
         */
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

複製程式碼

錯誤處理異常導致執行緒無法停止:


正確做法:丟擲異常,而異常的真正處理,應該交給呼叫它的那個函式。
低層方法,丟擲異常,呼叫者,就只有Surround with try/catch了。

正確程式碼如下:

package stopthreads;

import static java.lang.Thread.sleep;

/**
 * 描述:  catch了InterruptionException之後的優先選擇:在方法簽名中丟擲異常。
 * 那麼,在run()中就會強制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() throws InterruptedException {

        /**
         * 錯誤做法:這樣做相當於就把異常給吞了,導致上層的呼叫無法感知到有異常
         * 正確做法應該是,丟擲異常,而異常的真正處理,應該叫個呼叫它的那個函式。
         */
       /* try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

       sleep(2000);

    }
}

複製程式碼

總結:

2.3.2 最佳實踐二:不想或無法傳遞:恢復中斷(再次人為的手動把中斷恢復)

在低層方法中可以try-catch,但是一定要新增 Thread.currentThread().interrupt();

package stopthreads;

import static java.lang.Thread.sleep;

public class RightWayStopThreadInProduction2 implements Runnable{

    @Override
    public void run() {

        while(true){
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("執行緒中斷");
                break;
            }
            reInterrupt();
        }

    }

    private void reInterrupt() {

        try {
            sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction2());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }
}

複製程式碼

結果:

總結:如果不能丟擲中斷,要怎麼做?

如果不想或無法傳遞InterruptedException(例如用run方法的時候,就不讓該方法throws InterruptedException), 那麼應該選擇在catch子句中呼叫Thread.currentThread() interrupt()來恢復設定中斷狀態,以便於在後續的執行依然能夠檢查到剛才發生了中斷。具體程式碼見上,在這裡,執行緒在sleep期間被中斷,並且由catch捕獲到該中斷,並重新設定了中斷狀態,以便於可以在下一個迴圈的時候檢測到中斷狀態,正常退出。

不應遮蔽中斷

2.4 正確停止帶來的好處

3. 停止執行緒的錯誤方法

3.1 錯誤停止一:被棄用的stop,suspend,resume方法

用stop()來停止執行緒,會導致執行緒執行一半突然停止,沒辦法完成一個基本單位的操作(程式碼中是一個連隊),會造成髒資料(有的連隊多領取少領取裝備)。

package threadcoreknowledge.createthreads.stopthreads;

/**
 * 描述:     錯誤的停止方法:用stop()來停止執行緒,會導致執行緒執行一半突然停止,沒辦法完成一個基本單位的操作(一個連隊),會造成髒資料(有的連隊多領取少領取裝備)。
 */
public class StopThread implements Runnable{

    @Override
    public void run() {
        //模擬指揮軍隊:一共有5個連隊,每個連隊10人,以連隊為單位,發放武器彈藥,叫到號的士兵前去領取
        for (int i = 0; i < 5; i++) {
            System.out.println("連隊" + i + "開始領取武器");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("連隊"+i+"已經領取完畢");
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new StopThread());
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.stop();
    }


}

複製程式碼

還有一種錯誤的理論: 即使用stop()不會釋放,monitor(監視器)鎖,會造成程式的卡死。 官方有明確說明,stop(),會釋放的monitor(監視器)。

3.2 錯誤停止二:用volatile設定boolean標記位

看似可行

package threadcoreknowledge.stopthreads.volatiledemo;

/**
 * 描述:     演示用volatile的侷限:part1 看似可行
 */
public class WrongWayVolatile implements Runnable {

    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍數。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;
    }
}

複製程式碼

如何不行

storage.put(num);處被阻塞了,無法進入新的一層while()迴圈中判斷,!Canceled 的值也就無法判斷

程式碼演示:java

package threadcoreknowledge.createthreads.wrongway.volatiledemo;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = new Producer(storage);
        Thread thread = new Thread(producer);
        thread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(storage.take()+"被消費");
            Thread.sleep(100);
        }
        System.out.println("消費者不需要更多資料了");
        /**
         *  一旦消費不需要更多資料了,我們應該讓生產者也停下來,
         *  但是實際情況,在 storage.put(num);處被阻塞了,無法進入新的一層while()迴圈中判斷,!Canceled 的值也就無法判斷
         */
        producer.canceled = true;
        System.out.println(producer.canceled);

    }
}
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 {
            //canceled為true,則無法進入
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍數,被放到倉庫中了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生產者結束執行");
        }
    }

}

class Consumer {

    BlockingQueue storage;

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

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

複製程式碼

結果:程式並沒有結束。

程式並沒有結束

進行修復

使用interrupt: 程式碼演示:

package threadcoreknowledge.createthreads.wrongway.volatiledemo;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class WrongWayVolatileFixed  {

    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(storage.take()+"被消費");
            Thread.sleep(100);
        }
        System.out.println("消費者不需要更多資料了");
        /**
         *  一旦消費不需要更多資料了,我們應該讓生產者也停下來,
         *  但是實際情況,在 storage.put(num);處被阻塞了,無法進入新的一層while()迴圈中判斷,!Canceled 的值也就無法判斷
         */
        producerThread.interrupt();

    }
    class Producer implements Runnable{

        BlockingQueue storage;

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

        @Override
        public void run() {
            int num = 0;
            try {
                //canceled為true,則無法進入
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍數,被放到倉庫中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生產者結束執行");
            }
        }

    }

    class Consumer {

        BlockingQueue storage;

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

        public boolean needMoreNums() {
            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}
複製程式碼

結果:程式正常結束。

程式正常結束

總結:

總結

4. 停止執行緒重要函式原始碼解析

4.1 interrupt()原始碼分析

interrupt()原始碼分析

4.2 判斷中斷的相關方法分析

判斷中斷的相關方法

4.2.1 static boolean interrupted()

原始碼如下:

  /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words,if this method were to be called twice in succession,the
     * second call would return false (unless the current thread were
     * interrupted again,after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
複製程式碼

該方法會呼叫private native boolean isInterrupted(boolean ClearInterrupted);並傳入'true'('true'代表是否清除當前的狀態。)來判斷執行緒的狀態,並返回true/false(返回true代表該執行緒已經被中斷,返回false,代表該執行緒在繼續進行)。並且在返回之後,會把執行緒的中斷狀態直接設為false。也就是說,直接把執行緒中斷狀態直接給清除了,這也是唯一能清除執行緒中斷狀態的方法。(由於private native boolean isInterrupted(boolean ClearInterrupted);native修飾,故無法呼叫)

4.2.2 boolean isInterruted()

和static boolean interrupted();一樣,都會返回當前執行緒的中斷狀態,但是isInterrpted()方法不會清除中斷狀態。

4.2.3 Thread.interrupted()作用物件

Thread.interrupted()作用物件實際是呼叫它的執行緒。與誰打點調並沒有關係,不管是Thread呼叫,還是Thread的例項(thread)呼叫,interrupted()方法並不關係,它只關心的是時他自己處於哪個執行緒,在哪個執行緒,就返回該執行緒的中斷狀態,並返回後進行清除。

小練習:想一想,下面程式會輸出什麼樣的結果。

package threadcoreknowledge.createthreads.stopthreads;

public class RightWayInterrupted {

    public static void main(String[] args) throws InterruptedException {

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
                }
            }
        });
        // 啟動執行緒
        threadOne.start();
        //設定中斷標誌
        threadOne.interrupt();
        //獲取中斷標誌
        System.out.println("isInterrupted: " + threadOne.isInterrupted());  //true
        //獲取中斷標誌並重置
        System.out.println("isInterrupted: " + threadOne.interrupted()); // 會清除中斷標誌位 false
        //獲取中斷標誌並重直
        System.out.println("isInterrupted: " + Thread.interrupted());  //兩次清除 true
        //獲取中斷標誌
        System.out.println("isInterrupted: " + threadOne.isInterrupted()); //ture
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}
複製程式碼

執行結果如下:

執行結果

5. 常見面試問題

5.1 如何正確停止一個執行緒?

可以從以下三個方面進行闡釋:

  1. 原理:用 interrupt 來請求執行緒停止而不是強制,好處是安全。
  2. 三方配合:想停止執行緒,要請求方、被停止方、子方法被呼叫方相互配合才行:
    a. 作為被停止方:每次迴圈中或者適時檢查中斷訊號,並且在可能丟擲 InterrupedException 的地方處理該中斷訊號;
    b. 請求方:發出中斷訊號;
    c. 被呼叫的優先在方法層面丟擲,而不是捕獲異常 InterrupedException,如果不能丟擲異常,也可以在try-catch中再次設定中斷狀態;
  3. 最後再說錯誤的方法:stop/suspend 已廢棄,volatile 的 boolean 無法處 理長時間阻塞的情況

5.2 如何處理不可中斷的阻塞(例如搶鎖時 ReentrantLock.lock()或者 Socket I/O 時無法響應中斷,那應該怎麼讓該執行緒停止呢?)

如果執行緒阻塞是由於呼叫了 wait(),sleep() 或 join() 方法,你可以中斷執行緒,通過丟擲 InterruptedException 異常來喚醒該執行緒。 但是對於不能響應 InterruptedException 的阻塞,很遺憾,並沒有一個通用的 解決方案。 但是我們可以利用特定的其它的可以響應中斷的方法,比如:ReentrantLock.lockInterruptibly(),比如:關閉套接字使執行緒立即返回等方法 來達到目的。