Java JUC 抽象同步佇列AQS解析

神祕傑克 發表於 2022-01-26
Java

抽象同步佇列 AQS 解析

AQS——鎖的底層支援

AbstractQueuedSynchronizer 抽象同步佇列簡稱 AQS,它是實現同步器的基礎元件,併發包中的鎖底層都是使用 AQS 來實現的,下面看下 AQS 的類圖結構。

image-20220119113503475

該圖可知,AQS 是一個FIFO雙向佇列,其內部通過節點 head 和 tail 記錄隊首和隊尾的元素,佇列元素型別為Node

其中 Node 裡的 thread 變數用來存放進入 AQS 佇列裡的執行緒,而 SHARED 用來標記執行緒是獲取共享資源時被阻塞掛起放入 AQS 佇列的;EXCLUSIVE 用來標記該執行緒是獲取獨佔資源時被掛起放入 AQS 佇列中;waitStatus 記錄當前執行緒等待狀態,可以為CANCELLED(執行緒取消)SIGNAL(執行緒需要被喚醒)CONDITION(執行緒在條件佇列中等待)PROPAGATE(釋放共享資源時通知其他節點);prev 記錄當前節點的前驅節點,next 則是後驅節點。

在 AQS 中維持了一個單一的狀態資訊state,可以通過 getState、setState、compareAndSetState 函式修改值。

  • 對於 ReentrantLock 的實現,state 可以表示當前執行緒獲取鎖的次數
  • 對於讀寫鎖 ReentrantReadWriteLock,state 的高 16 位表示讀狀態,也就是獲取該鎖的次數,低 16 位表示獲取到寫鎖執行緒可重入的次數
  • 對於 Semaphore 來說,state 表示當前可用訊號的個數
  • 對於 CountDownlatch 來說,state 用來表示計數器當前的值

AQS 有個內部類 ConditionObject,它用來結合鎖實現執行緒同步。ConditionObject 可以直接訪問 AQS 物件內部的變數,比如 state 狀態值和佇列。

ConditionObject 是條件變數,每個條件變數對應一個條件佇列(單向連結串列佇列),用來存放呼叫條件變數的 await 方法後被阻塞的執行緒,而 firstWaiter 表示隊首元素,lastWaiter 表示隊尾元素。

條件佇列

這裡我們先說一下 waitStatus 所表示的幾個狀態。

  • CANCELLED(值為:1):表示當前節點已取消排程。當 timeout 或被中斷(響應中斷的情況下),會觸發變更為此狀態,進入該狀態後的節點將不會再變化。
  • SIGNAL(值為:-1):表示後繼節點在等待當前節點喚醒。後繼節點入隊時,會將前繼節點的狀態更新為 SIGNAL。
  • CONDITION(值為:-2):表示節點等待在 Condition 上,當其他執行緒呼叫了 Condition 的 signal()方法後,CONDITION 狀態的節點將從條件佇列轉移到同步佇列中,等待獲取同步鎖。
  • PROPAGATE(值為:-3):共享模式下,前繼節點不僅會喚醒其後繼節點,同時也可能會喚醒後繼的後繼節點。
  • 值為:0:新節點入隊時的預設狀態。

對於 AQS 來說,執行緒同步的關鍵就是對狀態值 state 進行操作,根據 state 是否屬於一個執行緒,操作 state 的方式分為獨佔和共享。

獨佔方式下獲取資源通過:void acquire(int arg) 、void acquireInterruptibly(int arg)

獨佔方式下釋放資源通過:boolean release(int arg)

共享方式下獲取資源通過:void acquireShared(int arg)void acquireSharedInterruptibly(int arg)

共享方式下釋放資源通過:boolean releaseShared(int arg)

在獨佔方式中獲取資源與具體執行緒繫結的,也就是說如果一個執行緒獲取到資源就會標記是這個執行緒獲取到了,其他執行緒再通過操作 state 獲取資源就會發現該資源不是自己持有的,隨後阻塞。

比如獨佔鎖 ReentrantLock 的實現中:當一個執行緒獲取到了 ReentrantLock 鎖,在 AQS 內部首先使用 CAS 操作將 state 值從 0 改為 1,然後設定當前鎖的持有者為當前執行緒,當該執行緒再次獲取鎖時發現它就是鎖的持有者,則會把狀態值從 1 改為 2,也就是設定可重入次數,而當另外一個執行緒獲取鎖時發現自己並不是該鎖的持有者就會被放入 AQS 阻塞佇列後掛起。

而共享方式的獲取資源是和具體執行緒不相關的,當多個執行緒去請求資源時通過 CAS 獲取資源,當一個執行緒獲取到資源後,別的執行緒再去獲取時如果當前資源還可以滿足需要的話,則只需要通過 CAS 方式獲取即可。

比如 Semaphore 訊號量,當一個執行緒通過 acquire 方法獲取訊號量時,會首先看當前訊號量個數是否滿足需要,不滿足則將當前執行緒放入阻塞佇列,滿足則通過自旋 CAS 獲取訊號量。

在獨佔方式中,獲取和釋放資源流程如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

當一個執行緒呼叫了 acquire 方法獲取獨佔資源時,首先使用 tryAcquire 方法嘗試獲取資源,具體就是設定狀態變數 state 的值,成功即直接返回;失敗的話則將當前執行緒封裝為型別為 Node.EXCLUSIVE 的 Node 節點隨後插入到 AQS 阻塞佇列的尾部,並呼叫 LockSupport.park(this)掛起自己。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

當一個執行緒呼叫 release 方法時會嘗試使用 tryRelease 操作釋放資源,這裡也是設定狀態變數 state 的值,隨後呼叫 LockSupport.unpark(thread) 方法啟用 AQS 佇列中被阻塞的一個執行緒。被啟用的執行緒則使用 tryAcquire 嘗試,看當前變數 state 值是否還能滿足自己的需要,滿足則繼續向下執行,否則還是被放入佇列中掛起。

📢 需要注意:AQS 類並沒有提供 tryAcquire、tryRelease 方法,需要由具體子類來實現,根據不同場景使用 CAS 演算法嘗試修改 state 狀態值,並且 state 狀態值的增減代表什麼意義。

比如繼承自 AQS 實現的獨佔鎖 ReentrantLock,當 status 為 0 時表示鎖空閒,為 1 時表示鎖已經被佔用。在重寫 tryAcquire 時,在內部需要使用 CAS 演算法檢視當前 state 是否為 0,如果為 0 則使用 CAS 設定為 1,並設定當前鎖的持有者為當前執行緒,然後返回 true,如果 CAS 失敗則返回 false。

在共享方式中,獲取和釋放資源流程如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

當執行緒呼叫 acquireShared 獲取共享資源時,會首先通過 tryAcquireShared 來嘗試獲取資源,具體還是設定狀態變數 state 的值,成功直接返回,失敗則將當前執行緒封裝為型別 Node.SHARED 的 Node 節點後插入到 AQS 阻塞佇列尾部,並使用 LockSupport.park(this)掛起自己。

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

當執行緒呼叫 releaseShared 時還是通過嘗試 tryReleaseShared 方法來釋放資源,也是設定狀態變數 state 的值,隨後使用 LockSupport.unpark(thread)來啟用 AQS 阻塞佇列中被阻塞的一個執行緒,被啟用的執行緒使用 tryReleaseShared 方法檢視當前 state 是否還滿足自己需要,滿足則啟用執行緒繼續向下執行,否則還是被放入 AQS 佇列中並被掛起。

📢 同樣需要注意,AQS 類並沒有提供可用的 tryAcquireShared、tryReleaseShared 方法,需要子類去實現。

比如繼承自 AQS 實現的讀寫鎖 ReentrantReadWriteLock 裡面的讀鎖在重寫 tryAcquireShared 時,首先檢視寫鎖是否被其他執行緒持有,如果是則直接返回 false,否則使用 CAS 遞增 state 的高 16 位(在 ReentrantReadWriteLock 中,state 的高 16 位為獲取讀鎖的次數)。

⚠️ 基於 AQS 實現的鎖除了需要重寫上述介紹的方法外,還需要重寫 isHeldExclusively 方法,來判斷鎖是被當前執行緒獨佔還是被共享。

另外我們發現acquireInterruptibly(int arg)acquireSharedInterruptibly(int arg)都帶有 Interruptibly 關鍵字。那麼帶和不帶這個關鍵字有什麼區別?

其實不帶 Interruptibly 關鍵字方法表示不對中斷進行響應,也就是執行緒在呼叫不帶 Interruptibly 的方法獲取資源或者獲取失敗被掛起時,其他執行緒中斷該執行緒,那麼該執行緒不會因為被中斷而丟擲異常,繼續獲取資源或被掛起,也就是不對終端進行響應,忽略中斷

而帶 Interruptibly 關鍵字則是會丟擲 InterruptedException 異常並返回。

下面我們看一下 AQS 如何維護佇列,主要檢視入隊操作。

當一個執行緒獲取鎖失敗後該執行緒會被轉換為 Node 節點,然後使用 enq(final Node node)方法將該節點插入到 AQS 阻塞佇列。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;//(1)
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))//(2)
                tail = head;
        } else {
            node.prev = t;//(3)
            if (compareAndSetTail(t, node)) {//(4)
                t.next = node;
                return t;
            }
        }
    }
}

步驟圖

如上程式碼,在第一次迴圈的時候當 AQS 佇列狀態如圖(預設情況)所示,頭尾均指向 null;當執行到程式碼(1)時候,節點 t 指向了尾部節點,佇列狀態如圖步驟(1)所示,這時 t 為 null,執行程式碼(2)時候使用 CAS 演算法設定一個哨兵節點為頭節點,如果設定成功則讓尾部節點也指向哨兵節點,這時佇列狀態如圖步驟(2)所示。

接下來我們還需要插入 node 節點,所以在第二次迴圈後又執行到程式碼(1),佇列狀態如下圖步驟(3)所示;然後執行程式碼(3)設定 node 的前驅節點為尾部節點,佇列狀態如下圖步驟(4)所示;隨後通過 CAS 演算法來設定 node 節點為尾部節點,CAS 成功後佇列狀態如下圖步驟(5)所示;隨後將原來的尾部節點的後驅節點設定為 node 節點,就完成了雙向連結串列。佇列狀態如下圖步驟(6)所示。

步驟圖

AQS——條件變數的支援

synchronized 和條件變數一樣都可以實現執行緒同步,它們的不同在於 synchronized 同時只能和一個共享變數 notify 或 wait 方法實現同步,而 AQS 的一個鎖可以對應多個條件變數。

接下來我們看一下例子。

public static void main(String[] args) {
    final ReentrantLock lock = new ReentrantLock();// (1)
    final Condition condition = lock.newCondition();// (2)
    lock.lock(); // (3)
    try {
        System.out.println("begin wait...");
        condition.await(); // (4)
        System.out.println("end wait...");
    } catch (Exception e) {
        lock.unlock(); // (5)
    }
    lock.lock(); // (6)
    try {
        System.out.println("begin signal...");
        condition.signal(); // (7)
        System.out.println("end signal...");
    } catch (Exception e) {
        lock.unlock(); // (8)
    }
}

這段程式碼首先建立另一個獨佔鎖 ReentrantLock 物件,也是基於 AQS 實現的。

第二步使用建立的 Lock 物件的 newCondition()方法建立了一個 ConditionObject 變數,這個變數就是 Lock 鎖對應的一個條件變數。

📢 一個 Lock 物件可以建立多個條件變數。

第三步獲取獨佔鎖,隨後第四步呼叫條件變數的 await()方法阻塞掛起當前執行緒。當其他執行緒呼叫了條件變數的 signal()方法時,被阻塞的執行緒的才會從 await 處返回,需要注意,和呼叫 Object 的 wait()方法一樣,如果沒有獲取到鎖就呼叫的話,則會丟擲 IllegalMonitorStateException 異常。第五步釋放獲取的鎖。

在上面程式碼中,lock.newCondition()的作用其實是 new 了一個在 AQS 內部宣告的 ConditionObject 物件,ConditionObject 是 AQS 的內部類,可以訪問 AQS 內部的變數(例如狀態變數 state)和方法。在每個條件變數內部都維護了一個條件佇列(單向連結串列佇列),用來存放呼叫條件變數的 await()方法時被阻塞的執行緒。注意這個條件佇列和 AQS 佇列不是一回事

我們看一下 await()方法原始碼:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();

            // 建立新的node節點,並插入到條件佇列末尾(1)
            Node node = addConditionWaiter();
            // 釋放鎖並返回狀態位(2)
            int savedState = fullyRelease(node);
            int interruptMode = 0;
                      // 呼叫park方法阻塞掛起當前執行緒(3)
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //...
        }

該方法中,當執行緒呼叫條件變數的 await()方法時,在內部會構造一個型別為 Node.CONDITION 的 node 節點,然後將該節點插入到條件佇列末尾,之後當前執行緒會釋放獲取到的鎖,也就是操作 state 值,並被阻塞掛起。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

當另外一個執行緒呼叫條件變數的 signal 方法時(必須先呼叫鎖的 lock()方法獲取鎖),在內部會把條件佇列裡面的一個執行緒節點從條件佇列裡面移除並放入 AQS 的阻塞佇列裡面,然後啟用這個執行緒。

📢 需要注意的是,AQS 只提供了 ConditionObject 的實現,並沒有提供 newCondition 函式,需要子類實現。

下面看一下在 await()方法阻塞後,如何放入條件佇列的。

private Node addConditionWaiter() {
    //獲取尾部節點
    Node t = lastWaiter;
    // 如果lastWaiter不為空,則檢查該佇列是否有被Cancel的節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        //遍歷條件佇列節點,移除已被取消的節點
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //利用當前執行緒構建一個代表當前執行緒的節點
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node; //沒有尾節點就插入到頭節點
    else
        t.nextWaiter = node;//尾節點的後驅節點等於當前節點
    lastWaiter = node; //尾節點等於當前節點
    return node;
}

📢 注意:當多個執行緒呼叫 lock.lock()方法時,只有一個執行緒獲取到鎖,其他執行緒就會被轉換到 Node 節點插入到對應的 AQS 阻塞佇列,並自旋 CAS 嘗試獲取鎖。

如果獲取到鎖的執行緒又呼叫了對應的條件變數的 await 方法,則該執行緒會釋放獲取到的鎖,並被轉換為 Node 節點插入到條件變數對應的條件佇列裡面。

當另外一個執行緒呼叫條件變數的 signal 或者 signalAll 方法時,會把條件佇列裡面的一個或者全部 Node 節點移動到 AQS 的阻塞佇列裡面,等待時機獲取鎖。

總結:一個鎖對應一個 AQS 阻塞佇列,對應多個條件變數,每個條件變數都有自己的一個條件佇列。

實現自定義獨佔鎖

/**
 * @author 神祕傑克
 * 公眾號: Java菜鳥程式設計師
 * @date 2022/1/20 實現自定義獨佔鎖
 * @Description
 */
public class NonReentrantLock implements Lock, Serializable {

    //自定義實現AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        //是否持有鎖
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        //如果state == 0 嘗試獲取鎖
        @Override
        protected boolean tryAcquire(int arg) {
            assert arg == 1;
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //嘗試釋放鎖 設定state == 0
        @Override
        protected boolean tryRelease(int arg) {
            assert arg == 1;
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //提供條件變數介面
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

根據自定義鎖實現生產者消費者

/**
 * @author 神祕傑克
 * 公眾號: Java菜鳥程式設計師
 * @date 2022/1/20
 * @Description 生產者消費者模型
 */
public class LockTest {

    final static NonReentrantLock lock = new NonReentrantLock();
    final static Condition consumerCondition = lock.newCondition();
    final static Condition producerCondition = lock.newCondition();
    final static Queue<String> QUEUE = new LinkedBlockingQueue<>();
    final static int QUEUE_SIZE = 10;

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        // 啟消費者執行緒
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("消費者消費了" + lockTest.get());
                }
            }, "consumer_" + i).start();
        }
        // 啟動生產者執行緒
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(200));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    lockTest.put("物品-" + new Random().nextInt(1000));
                }
            }, "產品-" + i).start();
        }

    }

    private void put(String name) {
        //獲取獨佔鎖
        lock.lock();
        try {
            //如果佇列滿了,則等待
            while (QUEUE.size() == QUEUE_SIZE) {
                producerCondition.await();
            }
            QUEUE.add(name);
            System.out.println(Thread.currentThread().getName() + "生產了" + name);
            //喚醒消費執行緒
            consumerCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private String get() {
        String ret = "";
        //獲取獨佔鎖
        lock.lock();
        try {
            //如果佇列空了,則等待
            while (QUEUE.size() == 0) {
                consumerCondition.await();
            }
            //消費一個元素
            ret = QUEUE.poll();
            //喚醒生產執行緒
            producerCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return ret;
    }

}