Java執行緒的中斷

張曾經發表於2019-05-26

引言

Java沒有提供任何機制來安全地終止執行緒,但提供了中斷機制,即thread.interrupt()方法。執行緒中斷是一種協作式的機制,並不是說呼叫了中斷方法之後目標執行緒一定會立即中斷,而是傳送了一箇中斷請求給目標執行緒,目標執行緒會自行在某個取消點中斷自己。這種設定很有必要,因為如果不論執行緒執行到何種情況都立即響應中斷的話,很容易造成某些物件狀態不一致的情況出現。

正文

一、中斷相關的方法介紹

涉及到中斷的執行緒基礎方法有三個:interrupt()、isInterrupted()、interrupted(),它們都位於Thread類下。Thread類下還有一個

interrupt()方法:對目標執行緒傳送中斷請求,看其原始碼會發現最終是呼叫了一個本地方法實現的執行緒中斷;

interrupted()方法:返回目標執行緒是否中斷的布林值(通過本地方法實現),且返回後會重置中斷狀態為未中斷;

isInterrupted()方法:該方法返回的是執行緒中斷與否的布林值(通過本地方法實現),不會重置中斷狀態;

二、執行緒中斷

執行緒中斷可以按中斷時執行緒狀態分為兩類,一類是執行時執行緒的中斷,一類是阻塞或等待執行緒的中斷。有中斷時,執行時的執行緒會在某個取消點中斷執行,而處於阻塞或者等待狀態的執行緒大多會立即響應中斷,比如上一篇文章中提到的join、sleep等方法,這些方法在丟擲中斷異常的錯誤後,會重置執行緒中斷狀態為未中斷。但注意,獲取獨佔鎖的阻塞狀態與BIO的阻塞狀態不會響應中斷。而在JUC包中有在加鎖阻塞的過程中響應中斷的方法,比如lockInterruptibly()。

下面從三個問題來講述執行緒中斷

1、執行緒中斷的目的是什麼?

為什麼要進行執行緒中斷?有時是由於對於某種特定情況,我們知道當前執行緒無需繼續執行下去,此時可以中斷此執行緒;有時是遇到某些異常,需要中斷執行緒。具體什麼目的,還要看具體場景,但執行緒中斷的需求已經擺在那裡,肯定需要。

2、要如何處理執行緒中斷?

通常的處理方式有兩種,如果是業務層面的程式碼,則只需要做好中斷執行緒之後的業務邏輯處理即可,而如果是偏底層功能的執行緒中斷,則儘量將中斷異常丟擲(或者在catch中重新呼叫interrupt()來中斷執行緒),以告知上層方法本執行緒的中斷經歷。

3、JUC中對中斷的處理舉例

JUC中ReentrantLock常用的加鎖方法是lock(),還有一個響應中斷的加鎖方法lockInterruptibly()

lock()方法中的acquire(int arg)方法如下所示:

1 public final void acquire(int arg) {
2         if (!tryAcquire(arg) &&
3             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4             selfInterrupt();
5 }

在acquireQueued中會對執行緒的中斷狀態做判斷,如果中斷了則返回true,進入selfInterrupt()方法,恢復執行緒的中斷狀態。但注意此處是在獲取到鎖之後再響應中斷,在獲取到鎖之前不會做出響應。

1 static void selfInterrupt() {
2         Thread.currentThread().interrupt();
3 }

而看lockInterruptibly()方法:

 1 public void lockInterruptibly() throws InterruptedException {
 2         sync.acquireInterruptibly(1);
 3     }
 4 
 5 public final void acquireInterruptibly(int arg)
 6             throws InterruptedException {
 7         if (Thread.interrupted())
 8             throw new InterruptedException();
 9         if (!tryAcquire(arg))
10             doAcquireInterruptibly(arg);
11     }

它會先檢視中斷狀態,再獲取鎖。而如果在獲取鎖的過程中中斷過,則會在doAcquireInterruptibly方法中丟擲中斷異常。

 下面是我在本地模擬的lock阻塞中斷:

 1 public class ReentrantLockDemo {
 2     public static void main(String[] args) throws InterruptedException {
 3         System.out.println("main start");
 4         Thread thread1 = new Thread(new LockThreadDemo());
 5         Thread thread2 = new Thread(new LockThreadDemo());
 6         thread1.start();
 7         Thread.sleep(1000); // 確保thread1獲取到了鎖
 8         thread2.start(); // 此時thread2處於獲取鎖的阻塞狀態
 9         thread2.interrupt();
10         System.out.println("main end");
11     }
12 }
13 
14 class LockThreadDemo implements Runnable {
15     public static ReentrantLock lock = new ReentrantLock();
16     @Override
17     public void run() {
18         System.out.println(Thread.currentThread().getName() + "runnable run");
19         try {
20             lock.lock();
21             System.out.println(Thread.currentThread().getName() + "開始睡眠");
22             Thread.sleep(5000);
23             System.out.println(Thread.currentThread().getName() + "睡了5秒");
24         } catch (Exception e) {
25             System.out.println(Thread.currentThread().getName() + "runnable exception:" + e);
26         } finally {
27             lock.unlock();
28         }
29         System.out.println(Thread.currentThread().getName() + " over");
30     }
31 }

執行結果為:

main start
Thread-0runnable run
Thread-0開始睡眠
main end
Thread-1runnable run
Thread-0睡了5秒
Thread-0 over
Thread-1開始睡眠
Thread-1runnable exception:java.lang.InterruptedException: sleep interrupted
Thread-1 over

可以看到中斷了並沒有對獲取鎖產生影響,最後是sleep方法響應的中斷。

下面是我在本地模擬的lockInterruptibly()阻塞中斷:

 1 public class ReentrantLockInterruptableDemo {
 2     public static void main(String[] args) throws InterruptedException {
 3         System.out.println("main start");
 4         Thread thread1 = new Thread(new LockThreadInterruptableDemo());
 5         Thread thread2 = new Thread(new LockThreadInterruptableDemo());
 6         thread1.start();
 7         Thread.sleep(1000); // 確保thread1獲取到了鎖
 8         thread2.start(); // 此時thread2處於獲取鎖的阻塞狀態
 9         thread2.interrupt();
10         System.out.println("main end");
11     }
12 }
13 
14 class LockThreadInterruptableDemo implements Runnable {
15     public static ReentrantLock lock = new ReentrantLock();
16     @Override
17     public void run() {
18         System.out.println(Thread.currentThread().getName() + "runnable run");
19         try {
20             lock.lockInterruptibly();
21             System.out.println(Thread.currentThread().getName() + "開始睡眠");
22             Thread.sleep(5000);
23             System.out.println(Thread.currentThread().getName() + "睡了5秒");
24         } catch (Exception e) {
25             System.out.println(Thread.currentThread().getName() + "runnable exception:" + e);
26         } finally {
27             try {
28                 lock.unlock();
29             } catch (IllegalMonitorStateException e) {
30                 System.out.println("因執行緒" + Thread.currentThread().getName() + "提前中斷導致未獲取到鎖");
31             }
32         }
33         System.out.println(Thread.currentThread().getName() + " over");
34     }
35 }

結果為:

main start
Thread-0runnable run
Thread-0開始睡眠
main end
Thread-1runnable run
Thread-1runnable exception:java.lang.InterruptedException
因執行緒Thread-1提前中斷導致未獲取到鎖
Thread-1 over
Thread-0睡了5秒
Thread-0 over

結束語

對於執行緒中斷的處理比較常見,尤其是涉及到多執行緒的框架、元件中。而能否處理好執行緒中斷的各種情況,則體現出一個程式設計師對多執行緒掌握的熟練情況。每天進步一點,日拱一卒,加油!

 

相關文章