Java多執行緒17:中斷機制

五月的倉頡發表於2015-10-05

概述

之前講解Thread類中方法的時候,interrupt()、interrupted()、isInterrupted()三個方法沒有講得很清楚,只是提了一下。現在把這三個方法同一放到這裡來講,因為這三個方法都涉及到多執行緒的一個知識點----中斷機制。

Java沒有提供一種安全、直接的方法來停止某個執行緒,而是提供了中斷機制。中斷機制是一種協作機制,也就是說通過中斷並不能直接終止另一個執行緒,而需要被中斷的執行緒自己處理。有個例子舉個蠻好,就像父母叮囑出門在外的子女要注意身體一樣,父母說了,但是子女是否注意身體、如何注意身體,還是要看自己。

中斷機制也是一樣的,每個執行緒物件裡都有一個標識位表示是否有中斷請求(當然JDK的原始碼是看不到這個標識位的,是虛擬機器執行緒實現層面的),代表著是否有中斷請求。

 

三個中斷方法

上面說了,中斷標識位是JDK原始碼看不到的,是虛擬機器執行緒實現層面的。下面結合程式碼逐一看一下這三個方法的作用,以及為什麼中斷標識位是虛擬機器實現層面的:

1、interrupt()

 1 public void interrupt() {
 2     if (this != Thread.currentThread())
 3         checkAccess();
 4 
 5     synchronized (blockerLock) {
 6         Interruptible b = blocker;
 7         if (b != null) {
 8         interrupt0();        // Just to set the interrupt flag
 9         b.interrupt();
10         return;
11         }
12     }
13     interrupt0();
14     }
1 /* Some private helper methods */
2 private native void setPriority0(int newPriority);
3 private native void stop0(Object o);
4 private native void suspend0();
5 private native void resume0();
6 private native void interrupt0();

分兩部分看:

(1)第一部分的第8行註釋說得很清楚了,interrupt0()方法的作用是"Just to set the interrupt flag",即方法的作用僅僅是設定中斷標識位

(2)第二部分的第6行就是interrupt0()方法的原型,由於方法是被native修飾的,很明顯這是一個本地方法,是Java虛擬機器實現的

2、isInterrupted()

方法唯一的作用只是測試執行緒是否已經中斷,中斷標識位的狀態並不受到該方法的影響,看一下Java是如何實現這個方法的:

 1 /**
 2  * Tests whether this thread has been interrupted.  The <i>interrupted
 3  * status</i> of the thread is unaffected by this method.
 4  *
 5  * <p>A thread interruption ignored because a thread was not alive 
 6  * at the time of the interrupt will be reflected by this method 
 7  * returning false.
 8  *
 9  * @return  <code>true</code> if this thread has been interrupted;
10  *          <code>false</code> otherwise.
11  * @see     #interrupted()
12  * @revised 6.0
13  */
14 public boolean isInterrupted() {
15 return isInterrupted(false);
16 }
private native boolean isInterrupted(boolean ClearInterrupted);

注意一下第一部分的第2行和第3行,"The interrupted statis of the thread is unaffected by this method",即執行緒的中斷狀態不受到這個方法的影響。最終呼叫的是isInterrupted(boolean ClearInterrupted),這個方法是一個native的,看得出也是Java虛擬機器實現的。方法的引數ClearInterrupted,顧名思義,清除中斷標識位,這裡傳遞false,明顯就是不清除

3、interrupted()

方法的作用是測試當前執行緒是否已經中斷,執行緒的中斷標識位由該方法清除。換句話說,連續兩次呼叫該方法的返回值必定是false。看一下這個方法是如何實現的:

 1 /**
 2  * Tests whether the current thread has been interrupted.  The
 3  * <i>interrupted status</i> of the thread is cleared by this method.  In
 4  * other words, if this method were to be called twice in succession, the
 5  * second call would return false (unless the current thread were
 6  * interrupted again, after the first call had cleared its interrupted
 7  * status and before the second call had examined it).
 8  *
 9  * <p>A thread interruption ignored because a thread was not alive 
10  * at the time of the interrupt will be reflected by this method 
11  * returning false.
12  *
13  * @return  <code>true</code> if the current thread has been interrupted;
14  *          <code>false</code> otherwise.
15  * @see #isInterrupted()
16  * @revised 6.0
17  */
18 public static boolean interrupted() {
19 return currentThread().isInterrupted(true);
20 
private native boolean isInterrupted(boolean ClearInterrupted);

同樣,第2行和第3行的註釋已經寫得很清楚了,"The interrupted status of the thread is cleared by this method",即執行緒的中斷狀態由此方法清除。另外,interrupted()方法和isInterrupted()方法呼叫的是同一個native方法,無非這個方法傳入的是true,表示清除中斷標識位

此外,JDK API中有些類的方法也可能會呼叫中斷,比如FutureTask的cancel,如果傳入true則會在正在執行的非同步任務上呼叫interrupt()方法,又如ThreadPoolExecutor中的shutdownNow方法會遍歷執行緒池中的工作執行緒並呼叫執行緒的interrupt()方法。這些場景下只要程式碼沒有對中斷作出響應,那麼任務將一直執行下去。

 

中斷處理時機

這其實是一個很寬泛的、沒有標註答案的話題。顯然,作為一種協作機制,不會強求被中斷的執行緒一定要在某個點進行中斷處理。實際上,被中斷執行緒只需要在合適的時候處理即可,如果沒有合適的時間點,甚至可以不處理。"合適的時間點"就和業務邏輯密切相關了。

處理時機決定著程式的效率和響應的靈敏度。頻繁的檢查中斷可能會導致程式執行效率低下,較少的檢查則可能導致中斷請求得不到及時響應。在實際場景中,如果效能指標比較關鍵,可能需要建立一個測試模型來分析最佳的中斷檢測點,以平衡效能和響應靈敏性

 

執行緒中斷舉例

寫了這麼多理論,寫一個例子來演示一下中斷:

public static void main(String[] args) throws Exception
{
    Runnable runnable = new Runnable()
    {
        public void run()
        {
            while (true)
            {
                if (Thread.currentThread().isInterrupted())
                {
                    System.out.println("執行緒被中斷了");
                    return ;
                }
                else
                {
                    System.out.println("執行緒沒有被中斷");
                }
            }
        }
    };
    Thread t = new Thread(runnable);
    t.start();
    Thread.sleep(3000);
    t.interrupt();
    System.out.println("執行緒中斷了,程式到這裡了");
}

看一下執行結果:

...
執行緒沒有被中斷
執行緒沒有被中斷
執行緒沒有被中斷
執行緒沒有被中斷
執行緒沒有被中斷
執行緒中斷了,程式到這裡了
執行緒被中斷了

程式碼分為以下幾步:

1、main函式起一個t執行緒

2、main函式3秒鐘之後給t執行緒打一箇中斷標識位,表示t執行緒要中斷

3、t執行緒無限輪詢自己的中斷標識位,中斷了則列印、退出,否則一直執行

從控制檯上列印的語句看到,3秒鐘中斷後,列印出該列印的語句後,就停止了。那這種場景就是前面說的"頻繁地檢查",導致程式效率低下;那如果不頻繁地檢查呢,比如在while中的else分支中加上Thread.sleep(500),表示500ms即0.5s檢查一次,那這種場景就是前面說的"中斷得不到及時的響應"。

其實這個例子中,t執行緒完全可以不用去管這個中斷標識位的,不去檢查就好了,只管做自己的事情,這說明中斷標識位設不設定是別人的事情,處不處理是我自己的事情,沒有強制要求必須處理中斷。

但是,那些會丟擲InterruptedException的方法要除外。像sleep、wait、notify、join,這些方法遇到中斷必須有對應的措施,可以直接在catch塊中處理,也可以拋給上一層。這些方法之所以會丟擲InterruptedException就是由於Java虛擬機器在實現這些方法的時候,本身就有某種機制在判斷中斷標識位,如果中斷了,就丟擲一個InterruptedException。

相關文章