從 JDK 原始碼角度看 java 併發執行緒的中斷

超人汪小建發表於2017-05-12

執行緒的定義給我們提供了併發執行多個任務的方式,大多數情況下我們會讓每個任務都自行執行結束,這樣能保證事務的一致性,但是有時我們希望在任務執行中取消任務,使執行緒停止。在java中要讓執行緒安全、快速、可靠地停下來並不是一件容易的事,java也沒有提供任何可靠的方法終止執行緒的執行。

執行緒排程策略中有搶佔式和協作式兩個概念,與之類似的是中斷機制也有協作式和搶佔式。

歷史上Java曾經使用stop()方法終止執行緒的執行,他們屬於搶佔式中斷。但它引來了很多問題,早已被JDK棄用。呼叫stop()方法則意味著:

①將釋放該執行緒所持的所有鎖,而且鎖的釋放不可控。
②即刻將丟擲ThreadDeath異常,不管程式執行到哪裡,但它不總是有效,如果存在被終止執行緒的鎖競爭;

第一點將導致資料一致性問題,這個很好理解,一般資料加鎖就是為了保護資料的一致性,而執行緒停止伴隨所持鎖的釋放,很可能導致被保護的資料呈現不一致性,最終導致程式運算出現錯誤。

第二點比較模糊,它要說明的問題就是可能存在某種情況stop()方法不能及時終止執行緒,甚至可能終止不了執行緒。看如下程式碼會發生什麼情況,看起來執行緒mt因為執行了stop()方法將停止,按理來說就算execut方法是一個死迴圈,只要執行了stop()方法執行緒將結束,無限迴圈也將結束。其實不會,因為我們在execute方法使用了synchronized修飾,同步方法表示在執行execute時將對mt物件進行加鎖,另外,Thread的stop()方法也是同步的,於是在呼叫mt執行緒的stop()方法前必須獲取mt物件鎖,但mt物件鎖被execute方法佔用,且不釋放,於是stop()方法永遠獲取不了mt物件鎖,最後得到一個結論,使用stop()方法停止執行緒不可靠,它未必總能有效終止執行緒。

public class ThreadStop {
    public static void main(String[] args) {
        Thread mt= new MyThread();
        mt.start();
        try {
            Thread.currentThread().sleep(100);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        mt.stop();
    }
static class MyThread extends Thread {
    public void run() {
        execute();
    }
    private synchronized void execute() {
        while(true) {
        }
    }
}
}複製程式碼

經歷了很長時間的發展,Java最終選擇用一種協作式的中斷機制實現中斷。協作式中斷的原理很簡單,其核心是先對中斷標識進行標記,某執行緒設定某執行緒的中斷標識位,被標記了中斷位的執行緒在適當的時間節點會丟擲異常,捕獲異常後做相應的處理。實現協作中斷有三個要點需要考慮:

①是在Java層面實現輪詢中斷標識還是在JVM中實現;
②輪詢的顆粒度的控制,一般顆粒度要儘量小週期儘量短以保證響應的及時性;
③輪詢的時間節點的選擇,其實就是在哪些方法裡面輪詢,例如JVM將Thread類的wait()、sleep()、join()等方法都實現中斷標識的輪詢操作。

中斷標識放在哪裡?中斷是針對執行緒例項而言,從Java層面上看,標識變數放到執行緒中肯定再合適不過了,但由於由JVM維護,所以中斷標識具體由本地方法維護。在Java層面僅僅留下幾個API用於操作中斷標識,如下,

public class Thread{
    public void interrupt() {……}
    public Boolean isInterrupted() {……}
    public static Booleaninterrupted() {……}
}複製程式碼

上面三個方法依次用於設定執行緒為中斷狀態、判斷執行緒狀態是否中斷、清除當前執行緒中斷狀態並返回它之前的值。通過interrupt()方法設定中斷標識,假如在非阻塞執行緒則僅僅只是改變了中斷狀態,執行緒將繼續往下執行,但假如在可取消阻塞執行緒中,如正在執行sleep()、wait()、join()等方法的執行緒則會因為被設定了中斷狀態而丟擲InterruptedException異常,程式對此異常捕獲處理。

上面提到的三個要點:

  • 第一是輪詢在哪個層面實現,這個沒有特別的要求,在實際中只要不出現邏輯問題,在Java層面或JVM層面實現都是可以的,例如常用的執行緒睡眠、等待等操作是通過JVM實現,而java併發框架工具裡面的中斷則放到Java實現,不管在哪個層面上去實現,在輪詢過程中都一定要能保證不會產生阻塞。
  • 第二是要保證輪詢的顆粒度儘可能的小週期儘可能短,這關係到中斷響應的速度。
  • 第三點是關於輪詢的時間節點的選取。

針對三要點來看看java併發框架中是如何支援中斷的,主要在等待獲取鎖的過程中提供中斷操作,下面是虛擬碼。只需增加加紅加粗部分邏輯即可實現中斷支援,在迴圈體中每次迴圈都對當前執行緒中斷標識位進行判斷,一旦檢查到執行緒被標記為中斷則丟擲InterruptedException異常,高層程式碼對此異常捕獲處理即完成中斷處理。總結起來就是java併發工具獲取鎖的中斷機制是在Java層面實現的,輪詢時間節點選擇在不斷做嘗試獲取鎖操作過程中,每個迴圈的顆粒度比較小,響應速度得以保證,且迴圈過程不存在阻塞風險,保證中斷檢測不會失效。

if(嘗試獲取鎖失敗) {
    建立node
    使用CAS方式把node插入到佇列尾部
    while(true){
        if(嘗試獲取鎖成功並且 node的前驅節點為頭節點){
            把當前節點設定為頭節點
            跳出迴圈
        }else{
            使用CAS方式修改node前驅節點的waitStatus標識為signal
            if(修改成功){
                掛起當前執行緒
                if(當前執行緒中斷位標識為true)
                    丟擲InterruptedException異常
            }
        }
    }
}複製程式碼

判斷執行緒是否處於中斷狀態其實很簡單,只需使用Thread.interrupted()操作,如果為true則說明執行緒處於中斷位,並清除中斷位。至此java併發工具實現了支援中斷的獲取鎖操作。

本文從java發展過程分析了搶佔式中斷及協作式中斷,由於搶佔式存在一些缺陷現在已不推薦使用,而協作式中斷作為推薦做法,儘管在響應時間較長,但其具有無可比擬的優勢。

協作式中斷我們可以在JVM層面實現,同樣也可以在Java層面實現,例如JDK併發工具的中斷即是在Java層面實現,不過如果繼續深究是因為Java留了幾個API供我們操作執行緒的中斷標識位,這才使Java層面實現中斷操作得以實現。

對於java的協作式中斷機制有人肯定有人批評,批評者說java沒有搶佔式中斷機制,且協作式中斷機制迫使開發者必須維護中斷狀態,迫使開發者必須處理InterruptedException。但肯定者則認為,雖然協作式中斷機制推遲了中斷請求的處理,但它為開發人員提供更靈活的中斷處理策略,響應性可能不及搶佔式,但程式健壯性更強。

====廣告時間,可直接跳過====

鄙人的新書《Tomcat核心設計剖析》已經在京東預售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

=========================

相關閱讀:
從JDK原始碼角度看併發鎖的優化
從JDK原始碼角度看執行緒的阻塞和喚醒
從JDK原始碼角度看併發競爭的超時
從JDK原始碼角度看Java併發的公平性

歡迎關注:

從 JDK 原始碼角度看 java 併發執行緒的中斷

相關文章