從 JDK 原始碼角度看執行緒的阻塞和喚醒

超人汪小建發表於2017-04-25

目前在Java語言層面能實現阻塞喚醒的方式一共有三種:suspend與resume組合、wait與notify組合、park與unpark組合。

其中suspend與resume因為存在無法解決的竟態問題而被Java廢棄,同樣,wait與notify也存在竟態條件,wait必須在notify之前執行,假如一個執行緒先執行notify再執行wait將可能導致一個執行緒永遠阻塞,如此一來,必須要提出另外一種解決方案,就是park與unpark組合,它位於JDK的juc包下,應該也是因為當時編寫juc時發現java現有方式無法解決問題而引入的新阻塞喚醒方式,由於park與unpark使用的是許可機制,許可最大為1,所以unpark與park操作不會累加,而且unpark可以在park之前執行,如unpark先執行,後面park將不阻塞。

Java真正意義上的語言層面上的併發程式設計應該從併發專家Doug Lea領導的JSR-166開始,此規範請求向JCP提交了向Java語言中新增併發程式設計工具,即在jdk中新增java.util.concurrent工具包供開發者使用,開發者可以輕鬆構建自己的同步器,而在此之前併發過程中同步都只能依靠JVM內建的管程。

JDK的併發包中最重要的一個框架就是ASQ框架,它的阻塞和喚醒使用的是LockSupport類的park與unpark方法,分別呼叫的是Unsafe類的park與unpark本地方法。邏輯如下:

阻塞

if(嘗試獲取鎖失敗) {  
    建立node  
    使用CAS方式把node插入到佇列尾部  
    while(true){  
        if(嘗試獲取鎖成功 並且 node的前驅節點為頭節點){  
            把當前節點設定為頭節點  
            跳出迴圈  
        }else{  
            使用CAS方式修改node前驅節點的waitStatus標識為signal  
            if(修改成功)
                LockSupport.park();  
        }   
    }  
}複製程式碼

喚醒

if(嘗試釋放鎖成功){  
    LockSupport.unpark(下一節點包含的執行緒);  
}複製程式碼

假如一條執行緒參與鎖競爭,首先先嚐試獲取鎖,失敗的話建立節點並插入佇列尾部,然後再次嘗試獲取鎖,如若成功則不做其他任務處理直接返回,否則設定節點狀態為待執行狀態,最後使用LockSupport的park阻塞當前執行緒。前驅節點執行完後將嘗試喚醒後繼節點,使用的即是LockSupport的unpark喚醒。

總的來說,JDK提供的併發工具,在阻塞與喚醒操作方面由於suspend與resume存在各種各樣問題,必須使用LockSupport中提供的方法操作。

相關閱讀:
從JDK原始碼角度看併發競爭的超時
從 JDK 原始碼角度看 Java 併發的公平性

歡迎關注:

從 JDK 原始碼角度看執行緒的阻塞和喚醒

相關文章