從JDK原始碼角度看併發競爭的超時

超人汪小建發表於2019-02-25

JDK中的併發框架提供的另外一個優秀機制是鎖獲取超時的支援,當大量執行緒對某一鎖競爭時可能導致某些執行緒在很長一段時間都獲取不了鎖,在某些場景下可能希望如果執行緒在一段時間內不能成功獲取鎖就取消對該鎖的等待以提高效能,這時就需要用到超時機制。在JDK1.5之前並沒有對此支援,當時的併發控制職能通過JVM內建的synchronized關鍵詞實現鎖,但對一些特殊要求卻力不從心,例如超時取消控制。JDK1.5開始引入併發工具完美解決了此問題,JDK對併發執行緒開始提供超時的支援。

為了更精確地保證時間間隔統計的準確性,實現時使用了System.nanoTime()更為精確的方法,它能精確到納秒級別。超時機制的思想就是在不斷進行鎖競爭的同時記錄競爭的時間,一旦時間段超過指定的時間則停止輪詢直接返回,返回前對等待佇列中對應節點進行取消操作。往下看實現的邏輯:

if(嘗試獲取鎖失敗) {
    long lastTime = System.nanoTime();
    建立node
    使用CAS方式把node插入到佇列尾部
    while(true){
        if(嘗試獲取鎖成功 並且 node的前驅節點為頭節點){
            把當前節點設定為頭節點
            跳出迴圈
        }else{
            if (nanosTimeout <= 0){
                取消等待佇列中此節點
                跳出迴圈
            }
            使用CAS方式修改node前驅節點的waitStatus標識為signal
            if(修改成功)
                if(nanosTimeout > spinForTimeoutThreshold)
                    阻塞當前執行緒nanosTimeout納秒
            long now = System.nanoTime();
            nanosTimeout -= now - lastTime;
            lastTime = now;
        }
    }
}複製程式碼

上面正在鎖的獲取邏輯中新增超時處理,核心邏輯是不斷迴圈減去處理的時間消耗,一旦小於0就取消節點並跳出迴圈,其中有兩點必須要注意,一個是真正的阻塞時間應該是扣除了競爭入隊的時間後剩餘的時間,保證阻塞事件的準確性,我們可以看到每次迴圈都會減去相應的處理時間;另外一個是關於spinForTimeoutThreshold變數閥值,它是決定使用自旋方式消耗時間還是使用系統阻塞方式消耗時間的分割線,JDK併發工具包作者通過測試將預設值設定為1000ns,即如果在成功插入等待佇列後剩餘時間大於1000ns則呼叫系統底層阻塞,否則不呼叫系統底層,取而代之的是僅僅讓之在Java應用層不斷迴圈消耗時間,屬於優化的措施。

至此,JDK實現在獲取鎖的過程中提供了超時機制,超時的支援讓Java在併發方面提供了更完善的機制,更多的併發策略滿足開發者更多需求。

歡迎關注

從JDK原始碼角度看併發競爭的超時

相關文章