AbstractQueuedSynchronizer 佇列同步器(AQS)

斷風雨發表於2018-11-27

AbstractQueuedSynchronizer 佇列同步器(AQS)

佇列同步器 (AQS), 是用來構建鎖或其他同步元件的基礎框架,它通過使用 int 變數表示同步狀態,通過內建的 FIFO 的佇列完成資源獲取的排隊工作。(摘自《Java併發程式設計的藝術》)

我們知道獲取同步狀態有獨佔和共享兩種模式,本文先針對獨佔模式進行分析。

變數定義

private transient volatile Node head;
複製程式碼

head 同步佇列頭節點

private transient volatile Node tail;
複製程式碼

tail 同步佇列尾節點

private volatile int state;
複製程式碼

state 同步狀態值

Node - 同步佇列節點定義

volatile int waitStatus;
複製程式碼

waitStatus 節點的等待狀態,可取值如下 :

  • 0 : 初始狀態
  • -1 : SIGNAL 處於該狀態的節點,說明其後置節點處於等待狀態; 若當前節點釋放了鎖可喚醒後置節點
  • -2 : CONDITION 該狀態與 Condition 操作有關後續在說明
  • -3 : PROPAGATE 該狀態與共享式獲取同步狀態操作有關後續在說明
  • 1 : CANCELLED 處於該狀態的節點會取消等待,從佇列中移除
volatile Node prev;
複製程式碼

prev 指向當前節點的前置節點

volatile Node next;
複製程式碼

next 指向當前節點的後置節點

volatile Thread thread;
複製程式碼

thread 節點對應的執行緒也是指當前獲取鎖失敗的執行緒

Node nextWaiter;
複製程式碼

acquire()

獨佔模式下獲取同步狀態, 既是當前只允許一個執行緒獲取到同步狀態

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

從 acquire 方法中我們可以大概猜測下,獲取鎖的過程如下:

  • tryAcquire 嘗試獲取同步狀態, 具體如何判定獲取到同步狀態由子類實現
  • 當獲取同步狀態失敗時,執行 addWaiter 建立獨佔模式下的 Node 並將其新增到同步佇列尾部
  • 加入同步佇列之後,再次嘗試獲取同步狀態,當達到某種條件的時候將當前執行緒掛起等待喚醒

下面具體看下各個階段如何實現:

private Node addWaiter(Node mode) {
	// 繫結當前執行緒 建立 Node 節點
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 判斷同步佇列尾節點是否為空
    if (pred != null) {
    	// node 的前置節點指向佇列尾部
        node.prev = pred;
        // 將同步佇列的 tail 移動指向 node
        if (compareAndSetTail(pred, node)) {
        	// 將原同步佇列的尾部後置節點指向 node
            pred.next = node;
            return node;
        }
    }
    // tail 為空說明同步佇列還未初始化
    // 此時呼叫 enq 完成佇列的初始化及 node 入隊
    enq(node);
    return node;
}
複製程式碼
private Node enq(final Node node) {
	// 輪詢的方式執行
	// 成功入隊後退出
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
        	// 建立 Node, 並將 head 指向該節點
        	// 同時將 tail 指向該節點
        	// 完成佇列的初始化
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
        	// node 的前置節點指向佇列尾部
            node.prev = t;
            // 將同步佇列的 tail 移動指向 node
            if (compareAndSetTail(t, node)) {
            	// 將原同步佇列的尾部後置節點指向 node
                t.next = node;
                return t;
            }
        }
    }
}
複製程式碼

從程式碼中可以看出通過 CAS 操作保證節點入隊的有序安全,其入隊過程中如下圖所示:

AQS節點入隊過程

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 
        for (;;) {
        	// 獲取當前節點的前置節點
            final Node p = node.predecessor();
            // 判斷前置節點是否為 head 頭節點
            // 若前置節點為 head 節點,則再次嘗試獲取同步狀態
            if (p == head && tryAcquire(arg)) {
            	// 若獲取同步狀態成功
            	// 則將佇列的 head 移動指向當前節點
                setHead(node);
                // 將原頭部節點的 next 指向為空,便於物件回收
                p.next = null; // help GC
                failed = false;
                // 退出輪詢過程
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
    	// 若前置節點狀態為 -1 ,則說明後置節點 node 可以安全掛起了
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
        	// ws > 0 說明前置節點狀態為 CANCELLED , 也就是說前置節點為無效節點
        	// 此時從前置節點開始向佇列頭節點方向尋找有效的前置節點
        	// 此操作也即是將 CANCELLED 節點從佇列中移除
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        // 若前置節點狀態為初始狀態 則將其狀態設為 -1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
複製程式碼
private final boolean parkAndCheckInterrupt() {
	// 將當前執行緒掛起
    LockSupport.park(this);
    // 被喚醒後檢查當前執行緒是否被掛起
    return Thread.interrupted();
}
複製程式碼

從 acquireQueued 的實現可以看出,節點在入隊後會採用輪詢的方式(自旋)重複執行以下過程:

  • 判斷前置節點是否為 head, 若為 head 節點則嘗試獲取同步狀態; 若獲取同步狀態成功則移動 head 指向當前節點並退出迴圈
  • 若前置節點非 head 節點或者獲取同步狀態失敗,則將前置節點狀態修改為 -1, 並掛起當前執行緒,等待被喚醒重複執行以上過程

如下圖所示:

AQS-節點自旋活動圖

接下來我們看看同步狀態釋放的實現。

release

釋放同步狀態

public final boolean release(int arg) {
	// 嘗試釋放同步狀態
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        	// 喚醒後置節點
            unparkSuccessor(h);
        return true;
    }
    return false;
}
複製程式碼
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
    	// 將 head 節點狀態改為 0
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // 獲取後置節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
    	// 喚醒後置節點上所阻塞的執行緒
        LockSupport.unpark(s.thread);
}
複製程式碼

從上述程式碼,我們可以明白釋放同步狀態的過程如下:

  • 呼叫 tryRelease 嘗試釋放同步狀態,同樣其具體的實現由子類控制
  • 成功釋放同步狀態後,將 head 節點狀態改為 0
  • 喚醒後置節點上阻塞的執行緒

如下圖所示(紅色曲線表示節點自旋過程) :

AQS-釋放鎖

acquireInterruptibly()

獨佔模式下獲取同步狀態,不同於 acquire 方法,該方法對中斷操作敏感; 也就是說當前執行緒在獲取同步狀態的過程中,若被中斷則會丟擲中斷異常

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    if (Thread.interrupted())
    	// 檢查執行緒是否被中斷
    	// 中斷則丟擲中斷異常由呼叫方處理
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
複製程式碼
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 不同於 acquire 的操作,此處在喚醒後檢查是否中斷,若被中斷直接丟擲中斷異常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
        	// 丟擲中斷異常後最終執行 cancelAcquire
            cancelAcquire(node);
    }
}
複製程式碼
private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    // 若當前節點為 tail 節點,則將 tail 移動指向 node 的前置節點
    if (node == tail && compareAndSetTail(node, pred)) {
    	// 同時將node 前置節點的 next 指向 null
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
        	// 當前節點位於佇列中部    
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
            	// 將前置節點的 next 指向 node 的後置節點
                compareAndSetNext(pred, predNext, next);
        } else {
        	// 若 node 的前置節點為 head 節點則喚醒 node 節點的後置節點
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
複製程式碼

從 acquireInterruptibly 的實現可以看出,若執行緒在獲取同步狀態的過程中出現中斷操作,則會將當前執行緒對應的同步佇列等待節點從佇列中移除並喚醒可獲取同步狀態的執行緒。

tryAcquireNanos()

獨佔模式超時獲取同步狀態,該操作與acquireInterruptibly一樣對中斷操作敏感,不同在於超過等待時間若未獲取到同步狀態將會返回

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
複製程式碼
private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 計算等待到期時間
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
            	// 超時時間到期直接返回
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                // 按指定時間掛起s
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

節點的狀態

同步佇列中的節點在自旋獲取同步狀態的過程中,會將前置節點的狀態由 0 初始狀態改為 -1 (SIGNAL), 若是中斷敏感的操作則會將狀態由 0 改為 1 (CANCELLED)

同步佇列中的節點在釋放同步狀態的過程中會將同步佇列的 head 節點的狀態改為 0, 也即是由 -1(SIGNAL) 變為 0;

小結

本文主要分析了獨佔模式獲取同步狀態的操作,其大概流程如下:

  • 在獲取同步狀態時,AQS 內部維護了一個同步佇列,獲取狀態失敗的執行緒會被構造一個節點加入到佇列中並進行一系列自旋操作
  • 在釋放同步狀態時,喚醒 head 的後置節點去獲取同步狀態

相關文章