天天看點

Thread詳解2:停止與中斷1 中斷标志2 interrupt() 不會中斷一個正在運作的線程3 InterruptedException4 停止一個線程的技巧

我們知道線程的start方法,那麼,很自然地會想到停止一個線程使用stop,然而stop方法是“過時的”,“不安全”。stop()方法,直接終止線程,釋放線程所獲的資源,但是在釋放過程中會造成對象狀态不一緻,進而使程式進入未知的境地,已經很久不推薦使用了。是以,Java沒有提供一種安全直接的方法來停止某個線程,但是Java提供了中斷機制。在這裡要着重介紹的是Thread.interrupt() 方法,也就是要分析清楚java的中斷機制。先來看看JDK怎麼描述的:

中斷線程。

  1. 如果目前線程沒有中斷它自己(這在任何情況下都是允許的),則該線程的 checkAccess 方法就會被調用,這可能抛出 SecurityException。
  2. 如果線程在調用 Object 類的 wait()、wait(long) 或 wait(long, int) 方法,或者該類的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法過程中受阻,則其中斷狀态将被清除,它還将收到一個 InterruptedException。
  3. 如果該線程在可中斷的通道上的 I/O 操作中受阻,則該通道将被關閉,該線程的中斷狀态将被設定并且該線程将收到一個 ClosedByInterruptException。
  4. 如果該線程在一個 Selector 中受阻,則該線程的中斷狀态将被設定,它将立即從選擇操作傳回,并可能帶有一個非零值,就好像調用了選擇器的 wakeup 方法一樣。
  5. 如果以前的條件都沒有儲存,則該線程的中斷狀态将被設定。
  6. 中斷一個不處于活動狀态的線程不需要任何作用。
  7. 抛出:SecurityException - 如果目前線程無法修改該線程

上述文檔詳細介紹了在各種情況下調用interrupt方法的結果,是以看不明白不要緊,有些知識會在另外的博文裡介紹,這裡我先介紹一些基礎。

1 中斷标志

Java中斷機制是一種協作機制,也就是說通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己進行中斷。這好比是家裡的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎麼注意身體則完全取決于自己。

Java中斷模型也是這麼簡單,每個線程對象裡都有一個boolean類型的辨別(不一定就要是Thread類的字段,實際上也的确不是,這幾個方法最終都是通過native方法來完成的),代表着是否有中斷請求(該請求可以來自所有線程,包括被中斷的線程本身)。例如,當線程t1想中斷線程t2,隻需要線上程t1中将線程t2對象的中斷辨別置為true,然後線程2可以選擇在合适的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一樣。

2 interrupt() 不會中斷一個正在運作的線程

在此我先介紹另外兩個方法,這兩個方法有助于我們利用程式分析interrupt():

(1)public static boolean interrupted:注意,它是一個靜态方法,是一個類方法,測試目前線程是否已經中斷。線程的中斷狀态 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用将傳回 false(在第一次調用已清除了其中斷狀态之後,且第二次調用檢驗完中斷狀态前,目前線程再次中斷的情況除外)。源碼為:

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
           

是以,這是一個命名很不恰當,很有迷惑性的方法,它不僅僅測試了目前線程的是否已經中斷,而且會把中斷狀态清除。

(2)public boolean isInterrupted():注意,這是一個成員方法,是對象的方法。測試線程是否已經中斷。線程的中斷狀态不受該方法的影響。

public class MyThread extends Thread {
    private int count = ;

    @Override
    synchronized public void run() {
        super.run();
        // 注意這裡,我讓 MyThread 隻列印5次,5次列印結束,這個線程就結束了
        for (int i = ; i < ; i++) {
            count++;
            System.out.printf(this.getName() + " 第 %d 次列印。\n", count);
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        myThread.interrupt();
        System.out.println("線程的中斷狀态是:"+myThread.isInterrupted());
    }
}
           
線程的中斷狀态是:true
Thread- 第  次列印。
Thread- 第  次列印。
Thread- 第  次列印。
Thread- 第  次列印。
Thread- 第  次列印。
           

可見,線程myThread的中斷狀态已經被設定為true,但是它并沒有被停止,好像interrupt()沒有起到任何作用。這也就是上面介紹的,主線程main想讓myThread中斷,但是它沒有理會,依然執行。這也是JDK文檔中提到的第一種情況。

是以,interrupt() 方法隻是将目标線程的中斷狀态設定為true,至于是否對這種中斷進行處理,完全看這個線程本身,也就是我們的代碼是否處理這種情況。上述執行個體代碼我們做一點點修改,增加一個判斷中斷狀态的步驟:

public class MyThread extends Thread {
    private int count = ;

    @Override
    synchronized public void run() {
        super.run();
        System.out.println("線程已經開始執行了!");
        // 注意這裡,我讓 MyThread 隻列印5次,5次列印結束,這個線程就結束了
        for (int i = ; i < ; i++) {
            if(interrupted()){
                System.out.println("線程已經被中斷,我們不往下執行了!");
                break;
            }
            count++;
            System.out.printf(this.getName() + " 第 %d 次列印。\n", count);
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        myThread.interrupt();
        System.out.println("線程的中斷狀态是:"+myThread.isInterrupted());
    }
}
           
線程的中斷狀态是:true
線程已經開始執行了!
線程已經被中斷,我們不往下執行了!
           

降到這裡,相信大家對于interrupt()方法已經有了一個基本的了解,那麼,JDK中提到的第二種情況是什麼意思呢?其實很簡單,往下看。

3 InterruptedException

當我想使用Thread.sleep() 方法的時候,IDE就會提醒我要用try/catch包裹:

Thread詳解2:停止與中斷1 中斷标志2 interrupt() 不會中斷一個正在運作的線程3 InterruptedException4 停止一個線程的技巧

這是因為sleep方法本身就會去檢查線程的中斷狀态,如果現在的中斷狀态為true,它會抛出InterruptedException。關于這個異常我們來看看JDK文檔:

當線程在活動之前或活動期間處于正在等待、休眠或占用狀态且該線程被中斷時,抛出該異常。有時候,一種方法可能希望測試目前線程是否已被中斷,如果已被中斷,則立即抛出此異常。下列代碼可以達到這種效果:

if (Thread.interrupted())  // Clears interrupted status!
      throw new InterruptedException();
           

這個代碼好眼熟,不就是section 2 中給出的示例代碼嗎?對的,在java中有很多方法是自帶對中斷狀态的判斷的,不用我們像section 2中那樣自己去寫。比如除了sleep(),wait()和join()等也是一樣。還是上面的例子做一點點修改:

public class MyThread1 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println("線程已經開始執行了!");
        // 注意這裡,我讓 MyThread 隻列印5次,5次列印結束,這個線程就結束了
        for (int i = ; i < ; i++) {
            System.out.printf(this.getName() + " 第 %d 次列印。\n", i+);
            try {
                System.out.printf("即将開始第 %d 次sleep\n",i+);
                Thread.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("線程已經被中斷,不能進入sleep!");
            }
        }
    }

    public static void main(String[] args) {
        MyThread1 myThread = new MyThread1();
        myThread.start();
        myThread.interrupt();
    }
}
           
線程已經開始執行了!
Thread- 第  次列印。
即将開始第  次sleep
線程已經被中斷,不能進入sleep!
java.lang.InterruptedException: sleep interrupted
Thread- 第  at java.lang.Thread.sleep(Native Method)
    at easy.MyThread1.run(MyThread1.java:)
 次列印。
即将開始第  次sleep
Thread- 第  次列印。
即将開始第  次sleep
Thread- 第  次列印。
即将開始第  次sleep
Thread- 第  次列印。
即将開始第  次sleep
Thread- 第  次列印。
即将開始第  次sleep
           

也就是:

  1. main在啟動 myThread 之後,立刻将其中斷狀态設定為true;
  2. 結果,在第一次調用sleep方法時,該方法去檢查線程的中斷狀态,發現為true,就抛出了InterruptedException異常;
  3. 然後該方法還将 myThread 的中斷狀态改為false,是以接下來的運作沒有任何問題。

(再次強調一點,這裡的“中斷狀态”隻是為了講解友善的一種形象的說法,真正的原理在native方法中,我們不去深究。)

而文章的前面貼出的JDK文檔的第二種情況是指在sleep、wait、join的狀态下調用interrupt方法的情形,也就是說interrupt方法實際上會觸發這三個方法中的InterruptedException異常機制。

public class MyThread2 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.printf("線程 %s 已經啟動!接下來進入sleep狀态", this.getName());
        try {
            Thread.sleep(*);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("\n我睡覺被吵醒會咬人的!");
        }
    }

    public static void main(String[] args) {
        final MyThread2 myThread = new MyThread2();
        myThread.start();
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("\n中斷它!");
                myThread.interrupt();
            }
        }, );// 設定指定的時間time,此處為2000毫秒
    }
}
           
線程 Thread- 已經啟動!接下來進入sleep狀态
中斷它!
java.lang.InterruptedException: sleep interrupted

我睡覺被吵醒會咬人的!
    at java.lang.Thread.sleep(Native Method)
    at easy.MyThread2.run(MyThread2.java:13)

           

4 停止一個線程的技巧

section 2 中的代碼其實我故意忽略了一個問題,那就是我用interrupted判斷後結束程序看似已經結束了,其實不然,該代碼隻是結束了for循環:break。如果for循環的底下還有代碼,該代碼會繼續執行:

public class MyThread1 extends Thread {
    private int count = ;

    @Override
    synchronized public void run() {
        super.run();
        System.out.println("線程已經開始執行了!");
        // 注意這裡,我讓 MyThread 隻列印5次,5次列印結束,這個線程就結束了
        for (int i = ; i < ; i++) {
            if(interrupted()){
                System.out.println("線程已經被中斷,我們不往下執行了!");
                break;
            }
            count++;
            System.out.printf(this.getName() + " 第 %d 次列印。\n", count);
        }

        System.out.println("快看,我在for循環的底下");
    }

    public static void main(String[] args) {
        MyThread1 myThread = new MyThread1();
        myThread.start();
        myThread.interrupt();
    }
}
           
線程已經開始執行了!
線程已經被中斷,我們不往下執行了!
快看,我在for循環的底下
           

解決辦法1:異常法

用try/catch包裹代碼,邏輯代碼放入try中,通過interrupted判斷中斷,一旦發現則主動抛出異常,進而進入了catch塊中,for循環以下的代碼不會執行。

public class MyThread1 extends Thread {
    private int count = ;

    @Override
    synchronized public void run() {
        super.run();
        System.out.println("線程已經開始執行了!");
        // 注意這裡,我讓 MyThread 隻列印5次,5次列印結束,這個線程就結束了
        try {
            for (int i = ; i < ; i++) {
                if(interrupted()){
                    System.out.println("線程已經被中斷,我們不往下執行了!");
                    //break;
                    throw new InterruptedException("異常法"); // 主動抛出異常,throw關鍵字不要忘記
                }
                count++;
                System.out.printf(this.getName() + " 第 %d 次列印。\n", count);
            }

            System.out.println("快看,我在for循環的底下");
        } catch (InterruptedException e) {
            System.out.println("進入了catch塊了");
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        MyThread1 myThread = new MyThread1();
        myThread.start();
        try {
            Thread.sleep();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myThread.interrupt();
    }
}
           

結果:

Thread詳解2:停止與中斷1 中斷标志2 interrupt() 不會中斷一個正在運作的線程3 InterruptedException4 停止一個線程的技巧

解決辦法2:return

将break改為return