本節摘要: 介紹線程中斷的原理、阻塞方法以及代碼示例
一、為什么需要中斷
有哪些場景我們需要中斷線程?通常有以下幾點:
- 我們希望在指定的時間內完成任務,但是任務執行時間太久,例如調用第三方的接口,或者執行一個耗時的I/O操作,這時我們希望取消該任務
- 當多個線程執行一個任務,只要有一個執行成功即可,其他線程取消
- 嘗試獲取某個資源(例如獲取對象鎖),超過期望時間仍沒有獲取到,我們希望取消該任務
但是java并沒有辦法安全、直接的停止一個線程,不過java提供了中斷機制,它為安全停止正在執行的任務提供了更大的靈活性。中斷本質上是一種協作機制,之所以稱為“協作”機制,是因為當對線程執行中斷操作時,線程并不是立即中斷,而是線程本身根據業務場景來自行判斷,事實上,大多數情況下我們也不希望線程立即停止,例如:
- 一個線程正在修改一個狀態,如果此時中斷線程,會導致狀態不一致
- 處理一批任務,要么全成功,要么全失敗,如果執行一半時檢測到中斷狀態,需要將之前的任務狀態重置
因此,線程的中斷可以理解為一種"告知",而非"命令"。舉個例子:
電業局的人告知用戶欠費了,有的用戶響應很及時,馬上就繳費;有的用戶有些其它的事情要處理,要過幾天才能繳費。當然,用戶也可以不繳費,最終就會停電。大概就是這個道理,大家可以總行體會一下^_-。
二、阻塞方法
拋出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
結果說明:
- 主線程創建t1線程,并啟動t1,t1線程的主要工作是循環輸出++i,每次休眠100ms
- 主線程休眠500ms,調用intterupt()方法中斷t1線程
- t1線程在收到中斷請求后,會清除中斷狀態,并拋出中斷異常,因此在catch塊第一次調用isInterrupted()返回false,為了保留線程的中斷狀態,調用了intterupt()方法,再次調用isInterrupted()方法返回true
- 主線程再次休眠500ms,等待t1線程的終止操作結束
- 主線程輸出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
結果說明:
- 主線程創建t1線程并啟動,t1線程主要工作是通過while()循環輸出++i,每次休眠300ms,循環條件是isInterrupted()方法
- 主線程休眠100ms,調用t1線程的interrupt()方法
- 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
結果說明:
- 主線程首先調用test(),這是一個加鎖方法,獲取o的對象鎖后開始循環輸出++i
- 主線程創建t1線程,然后啟動,再中斷t1線程
- t1線程的run方法會獲取o的對象鎖,如果主線程此時沒有釋放鎖,t1線程將一直阻塞
- 雖然對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();
}
七、全篇總結
- 線程中斷是一種協作機制
- 對于阻塞方法(如sleep,wait,join)拋出的異常,不應該吞掉,可以向上繼續拋出異常,或者設置中斷狀態來保留中斷證據
- 因進入鎖塊(synchronized)引起阻塞的線程,中斷操作不會拋出中斷異常,只是設置了線程的中斷狀態為true
- 因I/O阻塞的線程,通用解決方案就是關閉導致任務阻塞的底層資源