Java併發之AQS詳解

拉夫德魯發表於2021-09-12

帶著問題閱讀

1、什麼是AQS,它有什麼作用,核心思想是什麼

2、AQS中的獨佔鎖和共享鎖原理是什麼,AQS提供的鎖機制是公平鎖還是非公平鎖

3、AQS在Java中有哪些實現,如何基於AQS實現自己的鎖控制

4、AQS除了提供鎖框架以外還提供了什麼能力

AQS介紹

AbstractQueuedSynchronizer(AQS)提供了一套可用於實現鎖同步機制的框架,不誇張地說,AQSJUC同步框架的基石。AQS通過一個FIFO佇列維護執行緒同步狀態,實現類只需要繼承該類,並重寫指定方法即可實現一套執行緒同步機制。

AQS根據資源互斥級別提供了獨佔和共享兩種資源訪問模式;同時其定義Condition結構提供了wait/signal等待喚醒機制。在JUC中,諸如ReentrantLockCountDownLatch等都基於AQS實現。

AQS框架

AQS原理

AQS的原理並不複雜,AQS維護了一個volatile int state變數和一個CLH(三個人名縮寫)雙向佇列,佇列中的節點持有執行緒引用,每個節點均可通過getState()setState()compareAndSetState()state進行修改和訪問。·

當執行緒獲取鎖時,即試圖對state變數做修改,如修改成功則獲取鎖;如修改失敗則包裝為節點掛載到佇列中,等待持有鎖的執行緒釋放鎖並喚醒佇列中的節點。

AQS模版方法

AQS內部封裝了佇列維護邏輯,採用模版方法的模式提供實現類以下方法:

tryAcquire(int);        // 嘗試獲取獨佔鎖,可獲取返回true,否則false
tryRelease(int);        // 嘗試釋放獨佔鎖,可釋放返回true,否則false
tryAcquireShared(int);  // 嘗試以共享方式獲取鎖,失敗返回負數,只能獲取一次返回0,否則返回個數
tryReleaseShared(int);  // 嘗試釋放共享鎖,可獲取返回true,否則false
isHeldExclusively();    // 判斷執行緒是否獨佔資源

如實現類只需實現獨佔鎖/共享鎖功能,可只實現tryAcquire/tryReleasetryAcquireShared/tryReleaseShared。雖然實現tryAcquire/tryRelease可自行設定邏輯,但建議使用state方法對state變數進行操作以實現同步類。

如下是一個簡單的同步鎖實現示例:

public class Mutex extends AbstractQueuedSynchronizer {
    
    @Override
    public boolean tryAcquire(int arg) {
        return compareAndSetState(0, 1);
    }
    
    @Override
    public boolean tryRelease(int arg) {
        return compareAndSetState(1, 0);
    }
    
    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        
        new Thread(() -> {
            System.out.println("thread1 acquire mutex");
            mutex.acquire(1);
            // 獲取資源後sleep保持
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch(InterruptedException ignore) {
                
            }
            mutex.release(1);
            System.out.println("thread1 release mutex");
        }).start();
        
        new Thread(() -> {
            // 保證執行緒2線上程1啟動後執行
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch(InterruptedException ignore) {
                
            }
            // 等待執行緒1 sleep結束釋放資源
            mutex.acquire(1);
            System.out.println("thread2 acquire mutex");
            mutex.release(1);
        }).start()
    }
}

示例程式碼簡單通過AQS實現一個互斥操作,執行緒1獲取mutex後,執行緒2的acquire陷入阻塞,直到執行緒1釋放。其中tryAcquire/acquire/tryRelease/releasearg引數可按實現邏輯自定義傳入值,無具體要求。

@param arg the acquire argument. This value is conveyed to {@link #tryAcquire} but is otherwise uninterpreted and can represent anyting you like.

AQS核心結構

Node

前文提到,在AQS中如果執行緒獲取資源失敗,會包裝成一個節點掛載到CLH佇列上,AQS中定義了Node類用於包裝執行緒。

Java併發之AQS詳解

Node主要包含5個核心欄位:

  • waitStatus:當前節點狀態,該欄位共有5種取值:
    • CANCELLED = 1。節點引用執行緒由於等待超時或被打斷時的狀態。
    • SIGNAL = -1。後繼節點執行緒需要被喚醒時的當前節點狀態。當佇列中加入後繼節點被掛起(block)時,其前驅節點會被設定為SIGNAL狀態,表示該節點需要被喚醒。
    • CONDITION = -2。當節點執行緒進入condition佇列時的狀態。(見ConditionObject)
    • PROPAGATE = -3。僅在釋放共享鎖releaseShared時對頭節點使用。(見共享鎖分析)
    • 0。節點初始化時的狀態。
  • prev:前驅節點。
  • next:後繼節點。
  • thread:引用執行緒,頭節點不包含執行緒。
  • nextWaitercondition條件佇列。(見ConditionObject)

獨佔鎖分析

acquire

public final void acquire(int arg) {
    // tryAcquire需實現類處理
    // 如獲取資源成功,直接返回
    if (!tryAcquire(arg) && 
        // 如獲取資源失敗,將執行緒包裝為Node新增到佇列中阻塞等待
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如阻塞執行緒被打斷
        selfInterrupt();
}

acquire核心為tryAcquireaddWaiteracquireQueued三個函式,其中tryAcquire需具體類實現。 每當執行緒呼叫acquire時都首先會呼叫tryAcquire,失敗後才會掛載到佇列,因此acquire實現預設為非公平鎖

addWaiter將執行緒包裝為獨佔節點,尾插式加入到佇列中,如佇列為空,則會新增一個空的頭節點。值得注意的是addWaiter中的enq方法,通過CAS+自旋的方式處理尾節點新增衝突。

acquireQueue線上程節點加入佇列後判斷是否可再次嘗試獲取資源,如不能獲取則將其前驅節點標誌為SIGNAL狀態(表示其需要被unpark喚醒)後,則通過park進入阻塞狀態。

參照流程圖,acquireQueued方法核心邏輯為for(;;)shouldParkAfterFailedAcquiretail節點預設初始狀態為0,當新節點被掛載到佇列後,將其前驅即原tail節點狀態設為SIGNAL,表示該節點需要被喚醒,返回true後即被park陷入阻塞。for迴圈直到節點前驅為head後才嘗試進行資源獲取。

release

release流程較為簡單,嘗試釋放成功後,即從頭結點開始喚醒其後繼節點,如後繼節點被取消,則轉為從尾部開始找阻塞的節點將其喚醒。阻塞節點被喚醒後,即進入acquireQueued中的for(;;)迴圈開始新一輪的資源競爭。

共享鎖分析

acquireShared & releaseShared

public final void acquireShared(int arg) {
    // 負數表示獲取共享鎖失敗,不同於tryAcquire的bool返回
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

acquireSharedreleaseShared整體流程與獨佔鎖類似,tryAcquireShared獲取失敗後以Node.SHARED掛載到隊尾阻塞,直到隊頭節點將其喚醒。在doAcquireShared與獨佔鎖不同的是,由於共享鎖是可以被多個執行緒獲取的,因此在首個阻塞節點被喚醒後,會通過setHeadAndPropagate傳遞喚醒後續的阻塞節點。

// doAcquireShared核心程式碼
final Node node = addWaiter(Node.SHARED);
...
for (;;) {
    final Node p = node.predecessor();
    if (p == head) {
        int r = tryAcquireShared(arg);
        if (r >= 0) {
            // r>=0 表示獲取鎖成功,調整頭結點並傳遞喚醒
            setHeadAndPropagate(node, r);
        }
    }
    ...
}

setHeadAndPropagatedoReleaseShared構成共享鎖喚醒的核心邏輯。

這兩方法的邏輯較為簡單,不再進行展開,主要對setheadAndPropagate的多節點喚醒判斷邏輯做出分析。

進入setHeadAndPropagate,首先需要明確的是,該函式的傳入引數propagate一定是非負數,接下來其喚醒主要為兩個判斷邏輯:

  • 如果propagate > 0,表示存在多個共享鎖可以獲取,可直接進行doReleaseShared喚醒阻塞節點。

  • 如果propagate = 0,表示僅當前節點可被喚醒,則有兩種情況:

    • h == null || h.waitStatus < 0,通常情況下h != null,現給出h.waitStatus < 0的場景。

    • (h = head) == null || h.waitStatus < 0的場景執行序列如下:

獨佔鎖共享鎖小結

1、獨佔鎖共享鎖預設都是非公平獲取策略,可能被插隊。

2、獨佔鎖只有一個執行緒可獲取,其他執行緒均被阻塞在佇列中;共享鎖可以有多個執行緒獲取。

3、獨佔鎖釋放僅喚醒一個阻塞節點,共享鎖可以根據可用數量,一次喚醒多個阻塞節點

ConditionObject

AQSNode除了組成阻塞佇列外,還在ConditionObject中得到應用,ConditionObject的核心定義為:

public class ConditionObject implements Condition, java.io.Serializable {
    ... 
    private transient Node firstWaiter;
    private transient Node lastWaiter;
    ...
}

ConditionObject通過Node也構成了一個FIFO的佇列,那麼ConditionObjectAQS提供了怎樣的功能呢?

public interface Condition {
    ...
    void await() throws InterruptedException;
    void signal();
    void signalAll();
    ...
}

檢視Condition介面的定義,可以看到其定義的方法與Object類的wait/notify/notifyAll功能是一致的。

Synchronized詳解中筆者曾對ObjectMonitor做過簡單介紹,其中ObjectMonitor包含_WaitSet_EntryList兩個佇列,分別用於儲存wait呼叫sychronized鎖競爭時掛起的執行緒,而AQS通過ConditionObject同樣也提供了wait/notify機制的阻塞佇列。

Condihttps://blog.csdn.net/anlian523/article/details/106319294/tionObject機制如上圖,在條件佇列中,Node採用nextWaiter組成單向連結串列,當持有鎖的執行緒發起condition.await呼叫後,會包裝為Node掛載到Condition條件阻塞佇列中;當對應condition.signal被觸發後,條件阻塞佇列中的節點將被喚醒並掛載到鎖阻塞佇列中。ConditionObject的佇列邏輯與前述的acquire/release大同小異,不再贅述。

參考

相關文章