AQS原始碼理解

飛奔的蛋蛋發表於2018-01-30

很多人對於Java執行緒的併發都是停留在Lock層面,通過Lock能夠將synchronized粗大的顆粒劃分為很小的顆粒度。然而很多人可能並沒有真正去了解Lock的實現原理。 而不妨一說,AQS就是依靠資料結構的FIFO queue和compareAndSet來現實了強大的併發控制。那麼今天就讓我們一起來“解剖”AQS,從資料結構角度入手,再到具體實現,一覽AQS的全貌。

AQS功能介紹

有些同學可能對AQS不太熟悉,那麼先介紹一下。AQS(AbstractQueuedSynchronizer.class)是ReentrantLock、CountDownLatch等併發工具實現的父類,由AQS來定義了誰能拿到資源、誰需要等待,子類負責搶奪順序的實現。

AQS提供了兩種鎖:獨佔鎖和共享鎖

Node

我們先來看第一個資料結構Node。Node將Thread抽象成Node,同時賦予Node狀態,用不同的狀態來控制Thread的park和unPark。 所以在這一節,你需要了解Node的不同狀態,請看下面摘錄的原始碼。

static final class Node {

    /** 
     * 下面四種都為Node的狀態
     * CANCELLED 表明執行緒取消
     * SIGNAL 表明成功拿到鎖的執行緒需要喚醒
     * CONDITION 表明執行緒按照定義的條件等待
     * PROPAGATE 表明下一個acquireShared無條件propagate(在共享鎖中使用)
     */
    static final int CANCELLED =  1;

    static final int SIGNAL    = -1;
    
    static final int CONDITION = -2;

    static final int PROPAGATE = -3;

    // ------------------------------------------------ //

    volatile int waitStatus;

    volatile Node prev;

    volatile Node next;

    // 關聯執行緒
    volatile Thread thread;

    Node nextWaiter;
}
複製程式碼

AbstractQueuedSynchronizer

再來看AbstractQueuedSynchronizer的資料結構,很明顯是一個典型的雙向連結串列,同時使用了state來控制併發。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     *      +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+	
 	 */

    private transient volatile Node head;

    private transient volatile Node tail;

    /**
     * 抽象出來的資源
     */
    private volatile int state;
}
複製程式碼

ReentrantLock實現 EXCLUSIVE(獨佔鎖)

以ReentrantLock為例,我們來看下如何使用AbstractQueuedSynchronizer。

  1. Sync實現AbstractQueuedSynchronizer,定義lock為抽象方法,派生出FairSync和NonfairSync。 FairSync.lock加入爭奪佇列,NonfairSync直接去拿資源,如果有執行緒正在使用,才加入佇列
  2. 呼叫ReentrantLock.lock的時候去呼叫Sync.lock

lock過程

以FairSync為例,lock 等價於 acquire(1);

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

看上面的程式碼,其實分為了兩步:

  1. tryAcquire(arg) --> FairSync自己實現 // 檢視前面是否有競爭佇列
  2. acquireQueued(addWaiter(Node.EXCLUSIVE), arg) // 加入佇列,模式是獨佔鎖
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        /**
         * 注意這裡自旋去獲取鎖
         */
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
          	/**
	         * 會去檢查前面的Node的狀態,當滿足一定條件後才會將執行緒Park住,注意這時沒有跳出迴圈
	         */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    	** //Tips: 注意這裡沒有Catch,在併發包很多類中都有這樣的用法,可以google看看**
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

unlock 過程

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) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /**
     * 成功後將後面的node轉為unpark
     */    
    Node s = node.next;
    // 存在後面的node有別的狀態的情況,不符合要求則繼續往後找
    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);
}
複製程式碼

Tips:建議大家看看LockSupport,注意區分LockSuppork.park和Thread.interrupted概念上的不同。 java執行緒阻塞中斷和LockSupport的常見問題: http://agapple.iteye.com/blog/970055

CountDownLatch實現 SHARD(共享鎖)

說完了獨佔鎖,我們來看看共享鎖,以CountDownLatch為例(CountDownLatch的實現比較簡單便於理解,如果想更好的使用AQS可以看看ReentrantReadWriteLock的實現)。

先說下CountDownLatch使用方式:

  1. CountDownLatch(int n);
  2. 工作執行緒呼叫CountDownLatch.countDown(),減少步驟1中的n
  3. 需要共享的執行緒呼叫CountDownLatch.await,當1中的n為0的時候,執行緒才執行。這裡可以是多個執行緒同時使用await,這裡就是共享鎖的使用場景之一。
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                	/**
                	 * 這裡和獨佔鎖不同
                	 */
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

看完程式碼是不是覺得和獨佔鎖基本一毛一樣。對,除了setHeadAndPropagate(node, r)這個。

setHeadAndPropagate的作用: 當一個shard的Node獲得了資源,那麼就會喚醒佇列中他之後的連續的shard節點,使其同時執行。 這就是共享鎖和獨佔鎖不同之處。

總結一下

AQS提供了子類接觸資源的方式,同時不同的喚醒方式,提供給了使用者獨佔或者共享等不同的鎖的使用方式。 這裡不得不再次感嘆資料結構之美妙,一個簡簡單單的Queue就玩出了獨佔、共享鎖等等花樣。所以,理解原始碼一個很好的方式就是讀懂它內在的資料結構。 折騰不止,學習不止。 望與君共勉~

資料來源

AQS原始碼分析之獨佔鎖和共享鎖: http://blog.csdn.net/luofenghan/article/details/75065001 JDK原始碼AQS: http://childe.net.cn/2017/02/14/JDK%E6%BA%90%E7%A0%81-AQS/

相關文章