解密JUC——構建鎖和同步器的AQS

AQS,本名:AbstractQueuedSynchronizer,是Java 5引入的一個併發工具類。

它提供了一個基於FIFO(先進先出)隊列,可以用於構建鎖或者其他相關同步裝置的基礎框架。

它的名字翻譯爲抽象隊列同步器,可以分爲三個詞:抽象、隊列、同步器。

正好不知道怎麼開始,那麼現在我們就以名字的三個詞作爲切入點。但是爲了邏輯講得清晰,我調了一下順序:同步器->抽象->隊列。

1、同步器

這個類裏面有一個最關鍵的屬性:state,int類型。我覺得可以理解成兩種意思,首先名稱是state,直接翻譯爲狀態(比如ReentrantLock裏面的state用法),然後類型是int,可以理解爲資源數量(比如Semaphore裏面的state用法)。

它提供了getState和setState方法,還有一個線程安全的compareAndSetState方法。重點是後面這個,利用Unsafe進行CAS操作(如果當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值,相關資料可以參考上一篇博客:https://blog.csdn.net/qq_31142553/article/details/94407361)。

正是因爲可以做到在併發場景下對state的修改是原子性的並且可以獲取修改結果,所以基於這個特性可以將它做成一些同步器(類比Redis的setNx命令和Zookeeper的創建節點)。

2、抽象

抽象,說明它是可以被子類繼承並且重寫其中的一些方法。官方也是這麼說的:Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released。用我不太擅長的英語翻譯過來就是:子類必須明確更改此狀態的受保護方法,並定義哪種狀態對於此對象意味着被獲取或被釋放。因此這個類提供了以下方法

  • tryAcquire(int):試圖在獨佔模式下獲取對象狀態。
  • tryRelease(int):試圖設置狀態來反映獨佔模式下的一個釋放。
  • tryAcquireShared(int):試圖在共享模式下獲取對象狀態。
  • tryReleaseShared(int):試圖設置狀態來反映共享模式下的一個釋放。
  • isHeldExclusively():如果對於當前(正調用的)線程,同步是以獨佔方式進行的,則返回 true。

它們的默認實現都是throw new UnsupportedOperationException();要求我們覆蓋這些方法,定義哪種狀態對於此對象意味着被獲取或被釋放。

這就用到了設計模式裏面的模板方法模式:抽象父類定義了一個算法的所有步驟,而將其中一些實現交給子類,以滿足不同的場景需要。

比如用作鎖的話,state可以定義兩個值:0表示未鎖定狀態,1表示鎖定狀態。

那麼加鎖就可以這樣實現

// Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
    assert acquires == 1; // Otherwise unused
    if (compareAndSetState(0, 1)) {
        return true;
    }
    return false;
}

釋放鎖可以這樣實現

// Release the lock by setting state to zero
protected boolean tryRelease(int releases) {
    assert releases == 1; // Otherwise unused
    if (getState() == 0) throw new IllegalMonitorStateException();
    setState(0);
    return true;
}

比如用作信號量(許可數量)的話,state可以表示剩餘的數量。

那麼獲取所需資源就可以這樣實現

protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

釋放資源就可以這樣實現

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

3、隊列

線程搶奪資源失敗的時候,需要將其放進等待隊列的末尾,然後將其掛起。等到資源釋放的時候,拿出等待隊列裏面的第一個線程,讓其繼續去爭搶資源。

因爲這種首尾都會修改的使用特點,採用鏈表的實現方式遠優於數組。定義了一個Note的內部類表示鏈表的節點,除了有用於維護鏈表連接關係的prev(前節點)和next(後節點)屬性外,還有thread用於保存線程信息、waitStatus表示節點狀態等。

下面是waitStatus的取值,注意默認值是沒有意義的0。

waitStatus表示節點的狀態

AbstractQueuedSynchronizer中使用head和tail兩個Note類型的屬性存儲鏈表的頭結點和尾節點,從而可以修改或者遍歷那個所謂的隊列(或者說鏈表)。

        
如果把三者結合起來,就成了下面這個算法。


Acquire:
     while (!tryAcquire(arg)) {
        enqueue thread if it is not already queued;
        possibly block current thread;
     }

 Release:
     if (tryRelease(arg))
        unblock the first queued thread;


我們看下ReentrantLock的非公平鎖是怎麼實現的

(1)lock方法

下面看下是怎麼加入等待隊列的(跟着上圖中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)部分)

(2)unlock方法

文章暫時講到這裏了,還有一些功能暫時沒講到,比如ConditionObject、超時、共享模式等,後續看情況補上吧。

AQS其實蠻複雜的,特別是隊列裏線程等待和喚醒的那部分,如果有講錯的地方,麻煩一定要在評論區裏留言哦,萬分感謝🙏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章