JUC源碼學習之AbstractQueuedSynchronizer

源碼基於的Oracle JDK版本爲:11.0.5

什麼是CLH隊列

簡單理解是一個雙向鏈表,鏈表中存放的是包含線程在內的信息,隊首的是正在執行的線程,後面的是等待執行的線程,如下圖所示:

CLH示意圖

Node概述

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. Each node of the queue otherwise serves as a specific-notification-style monitor holding a single waiting thread. The status field does NOT control whether threads are granted locks etc though. A thread may try to acquire if it is first in the queue. But being first does not guarantee success; it only gives the right to contend. So the currently released contender thread may need to rewait.

To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you just set the head field.

        +------+  prev +-----+       +-----+
head    |      | <---- |     | <---- |     |  tail
        +------+       +-----+       +-----+

Insertion into a CLH queue requires only a single atomic operation on “tail”, so there is a simple atomic point of demarcation from unqueued to queued. Similarly, dequeuing involves only updating the “head”. However, it takes a bit more work for nodes to determine who their successors are, in part to deal with possible cancellation due to timeouts and interrupts.

The “prev” links (not used in original CLH locks), are mainly needed to handle cancellation. If a node is cancelled, its successor is (normally) relinked to a non-cancelled predecessor. For explanation of similar mechanics in the case of spin locks, see the papers by Scott and Scherer at http://www.cs.rochester.edu/u/scott/synchronization/

We also use “next” links to implement blocking mechanics. The thread id for each node is kept in its own node, so a predecessor signals the next node to wake up by traversing next link to determine which thread it is. Determination of successor must avoid races with newly queued nodes to set the “next” fields of their predecessors. This is solved when necessary by checking backwards from the atomically updated “tail” when a node’s successor appears to be null. (Or, said differently, the next-links are an optimization so that we don’t usually need a backward scan.)

Cancellation introduces some conservatism to the basic algorithms. Since we must poll for cancellation of other nodes, we can miss noticing whether a cancelled node is ahead or behind us. This is dealt with by always unparking successors upon cancellation, allowing them to stabilize on a new predecessor, unless we can identify an uncancelled predecessor who will carry this responsibility.

CLH queues need a dummy header node to get started. But we don’t create them on construction, because it would be wasted effort if there is never contention. Instead, the node is constructed and head and tail pointers are set upon first contention.

Threads waiting on Conditions use the same nodes, but use an additional link. Conditions only need to link nodes in simple (non-concurrent) linked queues because they are only accessed when exclusively held. Upon await, a node is inserted into a condition queue. Upon signal, the node is transferred to the main queue. A special value of status field is used to mark which queue a node is on.

Node的內部結構如下:

)

Node的狀態

代碼及註釋

/** 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;
狀態 簡介
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.
CANCELLED This node is cancelled due to timeout or interrupt. Nodes never leave this state. In particular, a thread with cancelled node never again blocks.
CONDITION This node is currently on a condition queue. It will not be used as a sync queue node until transferred, at which time the status will be set to 0. (Use of this value here has nothing to do with the other uses of the field, but simplifies mechanics.)
PROPAGATE A releaseShared should be propagated to other nodes. This is set (for head node only) in doReleaseShared to ensure propagation continues, even if other operations have since intervened.
0 None of the above

The values are arranged numerically to simplify use. Non-negative values mean that a node doesn’t need to signal. So, most code doesn’t need to check for particular values, just for sign.

The field is initialized to 0 for normal sync nodes, and CONDITION for condition nodes. It is modified using CAS (or when possible, unconditional volatile writes).

具體用途可等後續結合AQS框架整體的功能來看。

ReentrantLock概覽

ReentrantLock開始窺探AQS框架,其大致成員變量、方法以及內部類結構如下:

ReentrantLock結構示意圖

ReentrantLock裏面有一個抽象內部類叫做Sync,繼承自AbstractQueuedSynchronizer,這個類有兩個子類分別表示兩種獲取加鎖的方式:公平鎖和非公平鎖。它們間的區別留待後續說面。從上面的圖中的①和②可以看出,FairSyncNonfairSync的獲取鎖的方式不同,釋放鎖的方法都是一樣的,即獲取鎖的公平與否,體現在如何獲取鎖上。**默認是非公平鎖。**即如代碼所言:

// ReentrantLock.java
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

這裏使用默認的實現NonfairSync來進行分析。通常,使用ReentrantLock加鎖的時候都會調用lock()方法,其實現如下:

// ReentrantLock.java
public void lock() {
		sync.acquire(1);
}

lock()方法的執行後達到的效果如下:

  • Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.(沒有線程佔用此鎖,佔鎖數+1,並立即返回)

  • If the current thread already holds the lock then the hold count is incremented by one and the method returns immediately.(當前線程已獲取此鎖,佔鎖數+1,並立即返回)

  • If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired, at which time the lock hold count is set to one.(別的線程已佔用此鎖,當前線程暫停等待獲取鎖)

sync.acquire(1)調用的是父類AQS的實現。從這裏進入ASQ的源代碼:

AQS框架

// AbstractQueuedSynchronizer.java
/**
* Acquires in exclusive mode, ignoring interrupts.  Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success.  Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success.  This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument.  This value is conveyed to
*        {@link #tryAcquire} but is otherwise uninterpreted and
*        can represent anything you like.
*/
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

嘗試獲取鎖

首先調用第一個方法tryAcquire(arg)來嘗試獲取鎖,成功返回true。但這個方法是一個需要子類去實現的方法:

// AbstractQueuedSynchronizer.java

// Main exported methods
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*
* <p>This method is always invoked by the thread performing
* acquire.  If this method reports failure, the acquire method
* may queue the thread, if it is not already queued, until it is
* signalled by a release from some other thread. This can be used
* to implement method {@link Lock#tryLock()}.
*
* <p>The default
* implementation throws {@link UnsupportedOperationException}.
*
* @param arg the acquire argument. This value is always the one
*        passed to an acquire method, or is the value saved on entry
*        to a condition wait.  The value is otherwise uninterpreted
*        and can represent anything you like.
* @return {@code true} if successful. Upon success, this object has
*         been acquired.
* @throws IllegalMonitorStateException if acquiring would place this
*         synchronizer in an illegal state. This exception must be
*         thrown in a consistent fashion for synchronization to work
*         correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

也就是說具體的實現類爲AQS的子類Sync的子類NonfairSynctryAcquire()的實現。

// ReentrantLock.java
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
// 最終的實現方法
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 獲取當前狀態,volatile變量state
    int c = getState();
    if (c == 0) { // 說明沒有線程獲取到該鎖
        // 此處可能發生有多個線程同時執行
        // 通過CAS設置值,保證當前state是在爲0的情況下才設置成acquires,即只有一個線程能夠執行if爲true時的語句
        if (compareAndSetState(0, acquires)) {
            // 設置正在執行的線程爲當前線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { // 如果當前線程已獲取鎖
        int nextc = c + acquires; // state加1
        if (nextc < 0) // overflow。一個線程一直獲取該鎖的次數超過int的最大值
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果該鎖被其他線程佔有,那麼直接返回獲取鎖失敗
    return false;
}

獲取鎖失敗後,將該線程等息息加入CLH隊列

獲取鎖失敗後,返回false,在acquire(int arg)方法中將執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,也就是將該線程加入到CLH隊列中,首先執行的是addWaiter(Node.EXCLUSIVE)。在這方法中,先創建一個新的Node節點,這裏傳來的是Node.EXCLUSIVE,即null,作爲其後繼。即:

/** Establishes initial head or SHARED marker. */
Node() {}

/** Constructor used by addWaiter. */
Node(Node nextWaiter) {
    this.nextWaiter = nextWaiter;
    THREAD.set(this, Thread.currentThread());
}
Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

接下來是一個死循環,也就是經典的for循環+CAS操作,這個操作的目的就是將當前節點,插入到CLH隊列的隊尾,也就是入隊操作。

// AbstractQueuedSynchronizer.java
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            // 問題1:爲什麼設置前驅需要CAS操作?
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else { // CLH隊列爲空
            // 初始化隊列
            // 問題2:爲什麼初始化隊列需要CAS操作?
            initializeSyncQueue();
        }
    }
}

以上代碼的作用不難理解,但是仔細思考還是存在兩個問題,分別是爲什麼設置前驅需要CAS操作?以及爲什麼初始化隊列需要CAS操作?

問題1:爲什麼設置前驅需要CAS操作?

// 簡單讀寫
final void setPrevRelaxed(Node p) {
    PREV.set(this, p);
}
// PREV是什麼
private static final VarHandle PREV;
PREV = l.findVarHandle(Node.class, "prev", Node.class);

PREV是一個VarHandle,可以用來對Node裏面的prev屬性執行一些操作,如簡單讀寫操作、volatile讀寫操作、CAS操作等。這是一個jdk8後面的版本纔出的一個用來替代Unsafe操作的一個工具。具體的用法會在後續的博客中進行信息的探討。這裏的PREV.set(this, p)並不是一個CAS操作,是一個普通的讀寫操作。volatile寫是PREV.setVolatile()、CAS是PREV.compareAndSet()。所以這是一個誤解,這裏並不存在CAS操作。

問題2:爲什麼初始化隊列需要CAS操作?

/**
 * Initializes head and tail fields on first contention.
 */
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node())))
        tail = h;
}

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;

private static final VarHandle HEAD;
HEAD = l.findVarHandle(AbstractQueuedSynchronizer.class, "head", Node.class);

既然是初始化,AQS中的head變量肯定爲null。如果不爲空,說明已經被別的線程初始化了,CAS操作會失敗,從而跳出initializeSyncQueue(),繼續進入for(;;)嘗試在新的隊列中將該Node入隊。。這種情況會出現在兩個線程同時去獲得該鎖,且此時該鎖沒有被任何線程獲得(即隊列爲空),同時執行完了addWaiter()中的if (oldTail != null)語句,因爲爲null,所以兩個線程都轉而去執行initializeSyncQueue()的前提下。

讓線程暫時停止、休息

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 前驅爲head,嘗試獲取鎖
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // 檢查是否可以讓當前線程暫時停止
            if (shouldParkAfterFailedAcquire(p, node))
                // 暫時停止 等被喚醒的時候,會爲interrupted賦值
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

判斷當前線程是否可以被暫時停止。如果前驅Node的狀態被設置成了Node.SIGNAL,那麼可以被停止;否則都不能被停止,返回繼續執行acquireQueued()中的for(;;)代碼。當不能被停止的時候,只有兩種情況,如下:

  • 前驅被取消

這時候就要一直往前找,直到狀態是沒有被取消的;

  • 前驅ode狀態不爲Node.SIGNAL

這時候就要先通過CAS的方式將前驅的狀態改成Node.SIGNAL

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
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.
            */
        return true;
    if (ws > 0) { // 前驅被取消
        /*
            * Predecessor was cancelled. Skip over predecessors and
            * indicate retry.
            */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // 前驅ode狀態不爲`Node.SIGNAL`
        /*
            * 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.
            */
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

暫停線程,如果被喚醒,將繼續執行後面的return Thread.interrupted();,然後繼續返回acquireQueued()執行for(;;)裏面的語句,如嘗試獲取鎖、尋找可用的前驅、停止線程等。

/**
 * Convenience method to park and then check if interrupted.
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

其實後面還有一個catch語句,這裏面做的事情是發生異常了,將該node從鏈表中移除掉,然後再拋出異常。

到這裏AQSReentrantLock基本上上鎖的流程結束了。

釋放鎖

ReentrantLockunlock()方法執行後,如果鎖被當前線程持有,那麼鎖持有數將會減1;如果鎖的持有數爲0,將直接釋放;如果當前線程不持有該鎖,那麼將拋出IllegalMonitorStateException異常。

// ReentrantLock.java
/**
 * Attempts to release this lock.
 *
 * <p>If the current thread is the holder of this lock then the hold
 * count is decremented.  If the hold count is now zero then the lock
 * is released.  If the current thread is not the holder of this
 * lock then {@link IllegalMonitorStateException} is thrown.
 *
 * @throws IllegalMonitorStateException if the current thread does not
 *         hold this lock
 */
public void unlock() {
    sync.release(1);
}

釋放鎖與上鎖的邏輯基本上類似,設計模式都是模板方法。釋放鎖的框架代碼都寫好了,具體怎麼釋放由子類自行實現。轉入AQS的框架代碼如下:

// AbstractQueuedSynchronizer.java
/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

再次進入子類ReentrantLock中,調用其內部類SynctryRelease()方法。其中需要注意的是:沒有線程持有該鎖時,返回true,否則返回false。

// ReentrantLock.java -> Sync
protected final boolean tryRelease(int releases) {
    // 持鎖數減去releases
    int c = getState() - releases;
    // 非本線程持有該鎖,拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 沒有線程持有該鎖時,返回true,否則返回false
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

回到AQS的框架代碼,將看到如果tryRelease()方法如果返回true,纔有可能執行後面的unparkSuccessor()方法。這個方法就是找到head的可用(等待着唄喚醒的線程)後繼,然後unpark()該線程,讓該線程醒過來,繼續執行acquireQueued()方法中的for(;;),讓它獲取到鎖。

// AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

公平鎖與非公平鎖

回過頭來看FairSyncNonfairSync之間的差別。正如前面所言,他們之間的區別在於獲取鎖的方法不一樣,上面的代碼是NonfairSync的方式,現在看一下公平鎖的實現:

// ReentrantLock.java -> NonfairSync
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

核心的思想大致是:查看隊列中是否有存在的其他的等待線程,處於等待狀態的最前面的那一個線程,如果不是本線程,那麼直接返回false。

// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
    Node h, s;
    if ((h = head) != null) {
        if ((s = h.next) == null || s.waitStatus > 0) {
            s = null; // traverse in case of concurrent cancellation
            for (Node p = tail; p != h && p != null; p = p.prev) {
                if (p.waitStatus <= 0)
                    s = p;
            }
        }
        if (s != null && s.thread != Thread.currentThread())
            return true;
    }
    return false;
}

也就是說FairSynctryAcquire()返回false,所以它將繼續執行AQS裏面的acquireQueued(),即準備進入等待隊列。充分體現了先來後到的公平性。

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

參考:https://www.cnblogs.com/waterystone/p/4920797.html

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