Java併發——AQS源碼解析

本文通過總結源碼學習,來分析瞭解下AQS的工作原理

AQS是juc包鎖實現的基礎框架,研究juc包源碼之前,AQS是必經之路
雖然說,平時項目中,我們幾乎不會有自己去繼承aqs實現鎖的需要,但是通過源碼瞭解aqs的機制和原理,有助於我們加深對各種鎖的理解,以及出現問題時排查的思路

AbstractQueuedSynchronizer抽象隊列同步器,CLH 鎖

The wait queue is a variant of a “CLH” (Craig, Landin, and Hagersten) lock queue. CLH locks are normally used for spinlocks. We instead use them for blocking synchronizers, but use the same basic tactic of holding some of the control information about a thread in the predecessor of its node. A “status” field in each node keeps track of whether a thread should block. A node is signalled when its predecessor releases.

雙向FIFO等待隊列,自旋鎖,使用隊列結點對象Node包裝要獲取鎖的線程
AQS通過一個狀態變量state,來標誌當前線程持有鎖的狀態。
state = 0時代表沒有持有鎖,> 0 代表持有鎖。
當隊列中一個結點釋放鎖時,會喚醒後繼阻塞的線程

內部類Node
static final class Node {
    /** 共享模式的Node */
    static final Node SHARED = new Node();
    /** 獨佔模式的Node */
    static final Node EXCLUSIVE = null;

    /** waitStatus value to indicate thread has cancelled */
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;

    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    // 等待隊列中當前結點線程
    volatile Thread thread;

    Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 獲取前驅結點
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

如下圖就是aqs同步隊列中的一個節點結構,它有兩個分別指向前後節點的指針,包含了當前線程thread,以及節點的狀態waitStatus
Node

waitStatus解釋
其實關於waitStatus,有點不好理解的是SIGNAL這個狀態
我們先來看一下源碼中是如何解釋的

/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;

    /**
     * Status field, taking on only the values:
     *   SIGNAL:     The successor of this node is (or will soon be)
     *               blocked (via park), so the current node must
     *               unpark its successor when it releases or
     *               cancels. To avoid races, acquire methods must
     *               first indicate they need a signal,
     *               then retry the atomic acquire, and then,
     *               on failure, block.
     */
    volatile int waitStatus;

SIGNAL:表明後繼結點需要被喚醒
該結點的後繼結點已經阻塞或將被阻塞,所以當前結點必須喚醒它的後繼結點當其釋放鎖或者取消時。

這裏先通過註釋有個初步概念,後續通過源碼再來具體解釋

獨佔式同步狀態的獲取和釋放

在獨佔模式下,同一時刻只能有一個線程持有鎖,其他線程都要等待

獲取鎖

acquire是個模板方法,先通過tryAcquire方法嘗試獲取鎖,獲取成功則修改aqs的狀態state > 0,失敗則加入等待隊列中
tryAcquire是抽象方法需要子類實現

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

如果當前線程嘗試獲取鎖失敗,則將Node添加到等待隊列中

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 尾結點不爲空的話,在尾部添加該結點
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾結點爲null,說明隊列此時爲空,自旋插入該結點
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 初始化隊列頭尾結點
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 初始化後在尾部插入該結點
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq方法將會初始化隊列,此時的aqs隊列會變成如下形式

注意看初始化head和node的區別

compareAndSetHead(new Node())
Node node = new Node(Thread.currentThread(), mode);

head被初始化爲一個空的node,它裏面是沒有包含線程信息的,而後面的node,會設置當前線程信息
也就是說aqs中真正的等待隊列是不包括head的,後面黃色的node纔是真正的等待獲取鎖的第一個節點
每次添加節點時,從鏈表尾部添加,然後讓tail引用指向最後一個節點

添加到等待隊列中,當前結點線程會自旋的去獲取鎖

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 拿到前驅結點
            final Node p = node.predecessor();
            // 前驅結點爲head,說明當前結點是第一個,嘗試獲取鎖
            if (p == head && tryAcquire(arg)) {
                // 更新頭結點
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 獲取鎖失敗,判斷是否需要阻塞當前線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

這裏我們可以看到,正如上面的圖所示,獲取鎖時會先判斷前驅節點是不是head,如果是head就去嘗試獲取鎖,獲取成功則自己變爲head,這就是head的其中一個作用

獲取鎖失敗,要判斷當前結點線程是否應該被阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 判斷前驅結點的狀態
    int ws = pred.waitStatus;
    // 狀態爲SIGNAL,直接返回true
    if (ws == Node.SIGNAL)
        return true;
    // 狀態大於0,前驅節點是取消狀態
    if (ws > 0) {
        // 向前尋找非取消狀態的node
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 狀態爲0或PROPAGATE,將其更新爲signal
        // 會再次循環嘗試獲取鎖,如果失敗,在下一次循環就會阻塞當前線程
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

看到這裏我們需要再回過頭來理解下node中的waitStatus
該方法是判斷當前線程是否應該被阻塞,可以看到,這裏去判斷前驅結點的狀態,如果爲SIGNAL,則直接返回true,調用LockSupport.park阻塞當前線程
然而註釋中說的是SIGNAL表示後繼結點需要被喚醒(unpark)
網上看到很多博文的解釋是該狀態表示要阻塞後繼結點,說實話剛看到這個方法,的確會理解爲這個意思
其實我們結合註釋仔細想一下,如果前驅結點的狀態爲SIGNAL,那麼就說明當前線程在之後是會被喚醒的,這樣就可以放心的阻塞當前線程了,所以該方法通過判斷前驅結點的狀態,就是確保當前線程如果被阻塞了,它會在前驅結點釋放鎖時被喚醒

通過google相關資料,解釋如下
圖片截取自https://www.javarticles.com/2012/10/abstractqueuedsynchronizer-aqs.html

釋放鎖

和獲取鎖一樣,需要子類實現模板方法tryRelease

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // head不爲空且waitStatus不等於0
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

head不爲空說明等待隊列不爲空
head的waitStatus不爲0說明後繼結點線程需要被喚醒,獨佔式模式下狀態不爲0其實就是SIGNAL

喚醒後繼結點
注意這裏傳進來的node是head

 private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        // 將head的狀態重置爲0
        compareAndSetWaitStatus(node, ws, 0);

    // 如果後繼結點爲null或者被取消,則從tail開始向前,找到最後一個沒有被取消的結點
    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);
}
獲取鎖響應中斷

前面獲取鎖的方法是忽略線程中斷的,即使當前線程中斷了,還是會在等待隊列中等待被喚醒,aqs提供了acquireInterruptibly方法可以在獲取鎖時響應線程中斷

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

嘗試獲取鎖之前,如果當前線程已經中斷了,那麼直接throw異常

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())
                // 不是返回標記,而是直接拋出中斷異常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

可以看到此方法和acquireQueued區別就是,如果當前線程中斷的話,直接拋出異常,而不是返回boolean類型的中斷標記

private final boolean parkAndCheckInterrupt() {
    // 阻塞當前線程
    LockSupport.park(this);
    return Thread.interrupted();
}

看一下LockSupport.park方法的註釋

 * <ul>
 * <li>Some other thread invokes {@link #unpark unpark} with the
 * current thread as the target; or
 *
 * <li>Some other thread {@linkplain Thread#interrupt interrupts}
 * the current thread; or
 *
 * <li>The call spuriously (that is, for no reason) returns.
 * </ul>

在其他線程unpark喚醒當前線程,或者interrupt中斷當前線程時,該方法會返回
所以在等待隊列中阻塞的線程,如果被其他線程中斷,那麼會返回,然後拋出中斷異常,移出隊列,線程銷燬

獲取鎖響應中斷及超時

除了單純的響應線程中斷以外,AQS另外還提供了可控制等待超時時間的方法tryAcquireNanos

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;
    // 計算deadline
    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();
            // 小於等於0,已經超時
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

就是響應中斷的基礎上,另外加了等待超時時間的控制
等待隊列中的線程,被其他線程中斷,或者等待超時,則移出等待隊列

/**
 * The number of nanoseconds for which it is faster to spin
 * rather than to use timed park. A rough estimate suffices
 * to improve responsiveness with very short timeouts.
 */
static final long spinForTimeoutThreshold = 1000L;

1000納秒是非常短的時間了,無法做到完全精確,程序執行也會耗費一定時間,所以這裏粗略的估計在這個很短的時間內可以提高響應能力
<= spinForTimeoutThreshold 時將不再阻塞線程,直接再次自旋進行判斷

共享式同步狀態的獲取和釋放

共享模式,也就是同一時刻,可以有多個線程獲取到鎖,典型的應用就是讀寫鎖
在其他線程沒有獲取到寫鎖時,讀鎖可以有多個線程獲取到

獲取鎖

同獨佔模式一樣,共享模式提供了模板方法tryAcquireShared供子類實現
該方法不同於tryAcquire的地方是在於返回值爲int類型,而不是boolean

/**
 * @return a negative value on failure; zero if acquisition in shared
 *         mode succeeded but no subsequent shared-mode acquire can
 *         succeed; and a positive value if acquisition in shared
 *         mode succeeded and subsequent shared-mode acquires might
 *         also succeed, in which case a subsequent waiting thread
 *         must check availability. (Support for three different
 *         return values enables this method to be used in contexts
 *         where acquires only sometimes act exclusively.)  Upon
 *         success, this object has been acquired.
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

只截取了返回值說明,來看一下不同值代表的含義

  • 負數代表獲取鎖失敗
  • 等於0代表獲取鎖成功,但是共享模式下的其他結點將無法成功獲取鎖
  • 大於0代表獲取鎖成功,共享模式下的其他結點也可能會成功獲取鎖

嘗試獲取鎖失敗,加入等待隊列

private void doAcquireShared(int arg) {
    // 嘗試獲取鎖失敗,將線程添加到等待隊列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 前驅結點爲head,說明當前線程爲等待隊列中的第一個線程,嘗試獲取鎖
            if (p == head) {
                int r = tryAcquireShared(arg);
                // 大於等於0,獲取鎖成功
                if (r >= 0) {
                    // 共享模式下,喚醒其他等待結點
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    // tryAcquireShared結果大於0
    // 或者舊的head和新的head爲空或狀態被更新爲SIGNAL
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 後繼結點爲空或是共享模式,則喚醒後繼結點
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
釋放鎖
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 頭不爲空,且有結點在等待
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果狀態爲SIGNAL,則喚醒後繼結點
            if (ws == Node.SIGNAL) {
                // cas重置頭結點狀態
                // 因爲在releaseShared和setHeadAndPropagate方法都會調用該方法
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            // 共享模式下,將頭結點狀態設置爲PROPAGATE
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果前面喚醒了後繼結點,head將會被更新,則繼續循環
        // head沒有被更新,說明沒有後繼結點在阻塞,則跳出循環
        if (h == head)                   // loop if head changed
            break;
    }
}

同樣的,共享模式也提供了獲取鎖響應中斷,以及響應超時中斷的方法

總結
  • AQS提供了嘗試獲取鎖和釋放鎖的模板方法,子類根據具體場景實現
  • AQS中採用變量state來表示鎖持有的狀態,state大於0則表示持有鎖
  • 有獨佔式和共享式兩種,獨佔式同一時刻只能有一個線程持有鎖,共享式的典型應用就是讀寫鎖,寫鎖沒有被持有時,讀鎖是可以有多個線程獲取的
  • 兩種模式均提供了,不響應中斷,響應中斷,響應超時中斷的同步方法
  • head結點不是等待獲取鎖的線程,真正等待獲取鎖的結點是head後面開始的
  • 前驅結點的SIGNAL狀態,表示後繼結點可以被阻塞,當前驅結點釋放鎖的時候,會喚醒後繼阻塞結點

作爲一個“凡人”,有些地方還無法理解到Doug Lea大神的精髓和巧妙的設計,關於Java併發的研究纔剛剛開始,繼續努力

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