java多執行緒程式設計:你真的瞭解執行緒中斷嗎?

狂盗一枝梅發表於2024-06-14

java.lang.Thread類有一個 interrupt 方法,該方法直接對執行緒呼叫。當被interrupt的執行緒正在sleep或wait時,會丟擲 InterruptedException 異常。事實上, interrupt 方法只是改變目標執行緒的中斷狀態(interrupt status),而那些會丟擲InterruptedException 異常的方法,如wait、sleep、join等,都是在方法內部不斷地檢查中斷狀態的值,如果發現中斷,則丟擲InterruptedException異常。

interrupt方法

Thread例項方法:必須由其它執行緒獲取被呼叫執行緒的例項後,進行呼叫。實際上,只是改變了被呼叫執行緒的內部中斷狀態;

Thread.interrupted方法

Thread類方法:必須在當前執行執行緒內呼叫,該方法返回當前執行緒的內部中斷狀態,然後清除中斷狀態(置為false) ;

isInterrupted方法

Thread例項方法:用來檢查指定執行緒的中斷狀態。當執行緒為中斷狀態時,會返回true;否則返回false。

上面的一些說法比較抽象,為了驗證上述說法,寫幾個demo來驗證一下。

一、中斷和中斷檢查

1、interrupt方法可能不會中斷執行緒

首先得明確第一個問題:interrupt方法是用於中斷執行緒的方法,但是實際如果執行緒內沒有sleep等阻塞方法,它實際上並不會中斷執行緒,就算有sleep等方法執行,但是如果將異常捕獲了,那它也不會中斷執行緒的執行。看以下程式碼:

public class ThreadTest1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            int i = 0;
            while (true) {
                System.out.println(i++);
            }
        }
    }
}

執行該程式,將會進入死迴圈,不斷列印i的自增值,最後整型溢位也不會停止,主執行緒呼叫的interrupt方法根本無法阻止執行緒繼續執行。

正是之前所說的,“interrupt方法只是改變了被呼叫執行緒的內部中斷狀態“,那如何檢查執行緒的中斷狀態呢?

2、isInterrupted例項方法檢查中斷狀態

接下來我們呼叫Thread類的isInterrupted例項方法來檢查執行緒的中斷狀態

public class ThreadTest2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }   

        @Override
        public void run() {
            //檢查兩次中斷狀態,都是true
            System.out.println("first:"+Thread.currentThread().isInterrupted());
            System.out.println("second:"+Thread.currentThread().isInterrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

上述程式碼的執行結果如下

first:true
second:true
task mytask is over

主執行緒呼叫了中斷方法,執行緒內呼叫執行緒的isInterrupted方法,輸出都是true,表示都檢測到了中斷。為什麼要輸出兩次一模一樣的檢測結果呢?是為了驗證第一次呼叫的isInterrupted方法並沒有改變中斷狀態。

3、interrupted靜態方法檢查中斷狀態

interrupted方法是Thread類的靜態方法,它也能檢查當前執行緒的中斷狀態,但是隻能檢查一次:這個靜態方法有個天坑,它返回中斷狀態之後,會將中斷標誌復位成false,所以第二次呼叫該靜態方法就會發現中斷標誌被改變了。

public class ThreadTest3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }   

        @Override
        public void run() {
            //第一次呼叫返回中斷狀態,並將中斷狀態復位
            System.out.println("first :" + Thread.interrupted());
            //第二次呼叫返回復位後的中斷狀態
            System.out.println("second:" + Thread.interrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

中斷程式碼的輸出結果為

first :true
second:false
task mytask is over

可以看到,重複呼叫Thread.interrupted()方法,得到的結果並不一樣,原因就是第一次呼叫的時候中斷狀態被複位了。

二、中斷拋異常的情況討論

1、阻塞中斷並丟擲異常

既然是中斷方法,那它肯定能在某些情況下中斷執行緒的執行,什麼情況下呢?就是在大多數阻塞方法下,比如執行緒正在sleep、wait、join等。

看下以下程式碼示例

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("mytask"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("thread has been interrupt!");
            }
//            阻塞情況下中斷,丟擲異常後執行緒恢復非中斷狀態,即 interrupted = false
            System.out.println("isInterrupted: " +
                    Thread.currentThread().isInterrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

它的執行結果如下

thread has been interrupt!
isInterrupted: false
task mytask is over

執行緒在sleep期間被中斷並丟擲了InterruptedException異常,丟擲異常後立即重置了中斷狀態,所以接下來的檢查中斷方法得到的結果是false

2、interrupted不會中斷鎖阻塞

對於sleep等阻塞方法,遇到interrupt中斷方法會丟擲異常,但是對於鎖阻塞,則不會,看以下案例

public class ThreadTest4 {
    public static void main(String[] args) throws InterruptedException {
        Task mytask = new Task("mytask");
        Thread t1 = new Thread(mytask);
        Thread t2 = new Thread(mytask);
        t1.start();
        t2.start();
        t2.interrupt();
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            synchronized (this) {
                System.out.println(new Date() + ":" + Thread.currentThread().getName() + ": start run");
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    System.out.println(new Date() + ":" + Thread.currentThread().getName() + ":catch interrupted exception");
                
                }
            }
            System.out.println(new Date() + ":" + Thread.currentThread().getName() + ":" + Thread.currentThread().isInterrupted());
        }
    }
}

該程式執行結果如下

Fri Jun 14 15:11:52 CST 2024:Thread-0: start run
Fri Jun 14 15:11:55 CST 2024:Thread-1: start run
Fri Jun 14 15:11:55 CST 2024:Thread-0:false
Fri Jun 14 15:11:55 CST 2024:Thread-1:catch interrupted exception
Fri Jun 14 15:11:55 CST 2024:Thread-1:false

執行緒0持有鎖之後等待了3秒鐘,在等待期間,執行緒1嘗試進入方法區,但是拿不到鎖,所以進不去,進入鎖等待狀態,這時候主執行緒呼叫了執行緒1的執行緒中斷方法interrupt,但是執行緒1並沒有任何反映,等待執行緒0釋放了鎖之後,拿到鎖,這時候它開始執行sleep方法,由於執行緒中斷,丟擲了InterruptedException,所以它“沒睡”,直接列印資訊後結束了執行緒。

如果我們不想執行緒1丟擲異常,該怎麼做呢?其實只需要稍微修改一點程式碼:

System.out.println(new Date() + ":" + Thread.currentThread().getName() + ": start run");

將上面這行程式碼改成下面這樣子

System.out.println(new Date() + ":" + Thread.currentThread().getName() +":" + Thread.interrupted() + ": start run");

只是進入方法區之後加了一點點邏輯:Thread.interrupted()

再次執行上面的主方法,得到的結果為

Fri Jun 14 15:20:09 CST 2024:Thread-0:false: start run
Fri Jun 14 15:20:12 CST 2024:Thread-1:true: start run
Fri Jun 14 15:20:12 CST 2024:Thread-0:false
Fri Jun 14 15:20:15 CST 2024:Thread-1:false

現成1不拋異常了,而且正常等待了3秒鐘。。。可見,Thread.interrupted方法真是個隱藏的bug方法啊

三、總結

1、呼叫interrupt方法中斷執行緒實際上只是設定了中斷標誌,只有執行緒在執行sleep、wait等阻塞的方法的時候才會丟擲中斷異常,但是並不會中斷鎖阻塞;中斷丟擲異常後會重置中斷狀態為false

2、可以呼叫Thread類的isInterrupted例項方法檢測執行緒的中斷狀態,該方法不會重置中斷狀態為false

3、可以呼叫Thread類的interrupted靜態方法檢測執行緒的中斷狀態,該方法會重置中斷狀態為false,所以要慎用。

最後都看到這裡了,歡迎光臨我的個人部落格:https://blog.kdyzm.cn ~

相關文章