JAVA多線程10-基礎篇-線程終止

本節摘要: 介紹線程中斷的原理、阻塞方法以及代碼示例

一、為什么需要中斷

有哪些場景我們需要中斷線程?通常有以下幾點:

  1. 我們希望在指定的時間內完成任務,但是任務執行時間太久,例如調用第三方的接口,或者執行一個耗時的I/O操作,這時我們希望取消該任務
  2. 當多個線程執行一個任務,只要有一個執行成功即可,其他線程取消
  3. 嘗試獲取某個資源(例如獲取對象鎖),超過期望時間仍沒有獲取到,我們希望取消該任務

但是java并沒有辦法安全、直接的停止一個線程,不過java提供了中斷機制,它為安全停止正在執行的任務提供了更大的靈活性。中斷本質上是一種協作機制,之所以稱為“協作”機制,是因為當對線程執行中斷操作時,線程并不是立即中斷,而是線程本身根據業務場景來自行判斷,事實上,大多數情況下我們也不希望線程立即停止,例如:

  1. 一個線程正在修改一個狀態,如果此時中斷線程,會導致狀態不一致
  2. 處理一批任務,要么全成功,要么全失敗,如果執行一半時檢測到中斷狀態,需要將之前的任務狀態重置

因此,線程的中斷可以理解為一種"告知",而非"命令"。舉個例子:

電業局的人告知用戶欠費了,有的用戶響應很及時,馬上就繳費;有的用戶有些其它的事情要處理,要過幾天才能繳費。當然,用戶也可以不繳費,最終就會停電。大概就是這個道理,大家可以總行體會一下^_-。

二、阻塞方法

拋出InterruptedException的方法是阻塞方法,例如Thread.sleep(),Object.wait(),Thread.join()等。但是反過來阻塞方法不一定都拋出InterrruptedException異常,例如因進入鎖塊(synchronized)而阻塞的線程,并不拋出中斷異常。

三、java中處理中斷的方法

/**
*中斷本線程,如果當前線程處于阻塞狀態(如sleep,wait,join),調用線程的interrupt()方法,線程會清除中斷狀態,然后拋出中斷異常
*
*/
public void interrupt()

/**測試線程的中斷狀態并返回,同時清理線程的中斷狀態,有點拗口^_^
*  換句話說,如果連續兩次調用該方法,第二次返回false
*/
public static boolean interrupted()

/**測試線程是否被中斷,如果被中斷返回true,否則返回false*/
public boolean isInterrupted()

四、中斷原理

每個線程都擁有一個boolean屬性,用來表示該線程的中斷狀態,該屬性的初始值為false,當另外一個線程調用Thread.interrupt()方法時,會出現以下兩種情況:
1)如果線程執行的是阻塞方法(拋出中斷異常的方法,如join(),wait(),sleep()),會清除中斷狀態,并拋出InterruptedException異常,迅速響應中斷請求
2)如果是非阻塞方法,只是設置線程的中斷狀態,業務代碼可以根據中斷狀態以及業務場景來處理,可以選擇中斷線程,也可以忽略不管,或者設置中斷狀態后繼續執行。

五、代碼示例

5.1 處理InterruptedException 示例1

public class InterruptedExample1 {

    public static void main(String[] args) {
        Thread t1 = new MyThread("t1");
        t1.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "--"
                + t1.getName() + "--" + t1.getState() + "--" + t1.isInterrupted());
    }
}
class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        int i = 0;
        try {
            while (true) {
                System.out.println(++i);
                sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println("catch InterruptedException");
            System.out.println(getName() + "--" + getState() + "--" + isInterrupted());
            Thread.currentThread().interrupt();
            System.out.println(getName() + "--" + getState() + "--" + isInterrupted());
        }
    }
}

程序輸出:
1
2
3
4
5
catch InterruptedException
t1--RUNNABLE--false
t1--RUNNABLE--true
main--t1--TERMINATED--false

結果說明:

  1. 主線程創建t1線程,并啟動t1,t1線程的主要工作是循環輸出++i,每次休眠100ms
  2. 主線程休眠500ms,調用intterupt()方法中斷t1線程
  3. t1線程在收到中斷請求后,會清除中斷狀態,并拋出中斷異常,因此在catch塊第一次調用isInterrupted()返回false,為了保留線程的中斷狀態,調用了intterupt()方法,再次調用isInterrupted()方法返回true
  4. 主線程再次休眠500ms,等待t1線程的終止操作結束
  5. 主線程輸出t1的狀態為terminated,即t1線程已經終止

5.2 處理InterruptedException 示例2

public class InterruptedExample2 {
    public static void main(String[] args) {
        try {
            Thread t1 = new Thread(new TaskRunnable(), "t1");
            t1.start();
            TimeUnit.MILLISECONDS.sleep(100);
            t1.interrupt();
            System.out.println("ending");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class TaskRunnable implements Runnable {
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                System.out.println(++i);
                Thread.sleep(300);
            } catch (InterruptedException e) {
                System.out.println("catch the InterruptedException");
                Thread.currentThread().interrupt();//重新設置中斷標記
            }
        }
    }
}

結果輸出:
1
ending
catch the InterruptedException

結果說明:

  1. 主線程創建t1線程并啟動,t1線程主要工作是通過while()循環輸出++i,每次休眠300ms,循環條件是isInterrupted()方法
  2. 主線程休眠100ms,調用t1線程的interrupt()方法
  3. t1線程收到中斷請求后,會清除中斷狀態,并拋出中斷異常,因此需要在catch塊來重新設置中斷標記,使while循環條件為false,線程終止,如果不想終止線程,可以不必重新設置中斷標記

5.3 中斷非阻塞線程示例

public class InterruptedExample3 {
    public static void main(String[] args) {
        Thread t1 = new MyThread1("t1");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.interrupt();
    }
}
class MyThread1 extends Thread {
    public MyThread1(String name) {
        super(name);
    }
    @Override
    public void run() {
        while (true) {
            if (isInterrupted()) {
                System.out.println("isInterrupted is true");
            } else {
                System.out.println(" isInterrupted is false");
            }
        }
    }
}
結果輸出:
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
isInterrupted is false
.....
isInterrupted is true
isInterrupted is true
isInterrupted is true
isInterrupted is true
isInterrupted is true
....

結果說明:interrupt()方法只是設置中斷標記,不會終止線程,程序會一直輸出

5.4 不可中斷的阻塞示例(因獲取鎖而阻塞)

public class InterruptedExample4 {
    private static final Object o = new Object();
    public static void test() {
        int i = 0;
        synchronized (o) {
            while (i < 5) {
                System.out.println(i++);
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) {
        test();
        Thread t1 = new Thread(new MyRunnable2(o), "t1");
        t1.start();
        t1.interrupt();
    }
}
class MyRunnable2 implements Runnable {
    private Object o;
    public MyRunnable2(Object o) {
        this.o = o;
    }
    @Override
    public void run() {
        synchronized (o) {
            System.out.println("get the lock do something");
        }
    }
}

程序輸出:
0
1
2
3
4
get the lock do something

結果說明:

  1. 主線程首先調用test(),這是一個加鎖方法,獲取o的對象鎖后開始循環輸出++i
  2. 主線程創建t1線程,然后啟動,再中斷t1線程
  3. t1線程的run方法會獲取o的對象鎖,如果主線程此時沒有釋放鎖,t1線程將一直阻塞
  4. 雖然對t1執行了中斷操作,但是線程并沒有中斷,當獲取o的對象鎖后,線程繼續執行

5.5 中斷因I/O阻塞的線程

這個示例可以參考<<java編程思想>>第21章的示例,解決方案就是關閉導致任務阻塞的底層資源,例如socket 連接。

六、代碼示例總結

通過上面的示例,我們可以總結如下的代碼形式:

6.1 while循環在try...catch..塊內,這種方式適用于線程處于“阻塞”和“非阻塞”兩種狀態

   @Override
   public void run() {
       try {
           //根據isInterrupted()方法判斷是否被中斷
           while (!Thread.currentThread().isInterrupted()) {
              //do work
           }
       } catch (InterruptedException e) {
           //如果是阻塞方法(如join,wait,sleep)被中斷,會catch中斷異常,如果需要通調用
           //方中斷狀態,調用interrupt()方法設置中斷狀態為true
       }
   }

6.2 while循環在try...catch..塊外

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                //do work
            } catch (InterruptedException e) {
                //如果希望線程終止,此處必須重新設置中斷狀態,否則死循環
                Thread.currentThread().interrupt();
            }
        }
    }

6.3 如果調用阻塞方法(不在Runnable.run()內)又不知道如何處理InterruptedException,最簡單的辦法是直接拋出中斷異常,由調用方處理

 public static void test() throws InterruptedException {
        Thread.currentThread().wait();
    }

七、全篇總結

  1. 線程中斷是一種協作機制
  2. 對于阻塞方法(如sleep,wait,join)拋出的異常,不應該吞掉,可以向上繼續拋出異常,或者設置中斷狀態來保留中斷證據
  3. 因進入鎖塊(synchronized)引起阻塞的線程,中斷操作不會拋出中斷異常,只是設置了線程的中斷狀態為true
  4. 因I/O阻塞的線程,通用解決方案就是關閉導致任務阻塞的底層資源
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容