原始碼分析之AbstractQueuedSynchronizer

特立獨行的豬手發表於2017-03-28

java.util.concurrent包中,大部分的同步器都是基於AbstractQueuedSynchronizer(AQS)這個框架實現的。這個框架為同步狀態提供原子性管理、執行緒的阻塞和解除阻塞以及排隊提供了一種通用機制。

同步器一般包含2種方法,一種是acquire,另一種是releaseacquire操作阻塞執行緒,獲取鎖。release通過某種方式改變讓被acquire阻塞的執行緒繼續執行,釋放鎖。為了實現這2種操作,需要以下3個基本元件的相互協作:

  • 同步狀態的原子性管理
  • 執行緒的阻塞和解除阻塞
  • 佇列管理

同步狀態

    /**
     * The synchronization state.
     */
    private volatile int state;複製程式碼

AQS使用一個int變數來儲存同步狀態,並暴露出getStatesetState以及compareAndSet來讀取或更新這個狀態。並且用了volatile來修飾,保證了在多執行緒環境下的可見性。通過使用compare-and-swap(CAS)指令來實現compareAndSetState

這裡的同步狀態用int而非long,主要是因為64位long欄位的原子性操作在很多平臺上是使用內部鎖的方式來模擬實現的,這會使得同步器的會有效能問題。絕對多數int型的state足夠我們使用,但JDK也提供了longstate的實現:java.util.concurrent.locks.AbstractQueuedLongSynchronizer

阻塞

JDK1.5之前,阻塞執行緒和解除執行緒阻塞都是基於Java自身的監控器。在AQS中實現阻塞是用java.util.concurrent包的LockSuport類。方法LockSupport.park阻塞當前執行緒,直到有個LockSupport.unpark方法被呼叫。

佇列管理

AQS框架關鍵就在於如何管理被阻塞的執行緒佇列。提供了2個佇列,分別是執行緒安全Sync Queue(CLH Queue)、普通的Condition Queue

Sync Queue

Sync Queue是基於FIFO的佇列,用於構建鎖或者其他相關同步裝置。CLH鎖可以更容易地去實現取消(cancellation)超時功能,因此我們選擇了CLH鎖作為實現的基礎。

佇列中的元素Node是儲存執行緒的引用和執行緒狀態。NodeAQS的一個靜態內部類:

 static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        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;
    }複製程式碼

Node類的成員變數如上所示,主要負責儲存執行緒引用、佇列的前繼和後繼節點,以及同步狀態:

成員 描述
waitStatus 用來標記Node的狀態:
CANCELLED:1, 表示當前執行緒已經被取消
SIGNAL:-1,表示當前節點的後繼節點等待執行
CONDITION:-2, 表示當前節點已被加入Condition Queue
PROPAGATE:-3, 共享鎖的最終狀態是PROPAGATE
thread 當前獲取lock的執行緒
SHARED 表示節點是共享模式
EXCLUSIVE 表示節點是獨佔模式
prev 前繼節點
next 後繼節點
nextWaiter 儲存Condition Queue中的後繼節點

Node元素是Sync Queue構建的基礎。當獲取鎖的時候,請求形成節點掛載在尾部。而鎖資源的釋放再獲取的過程是從開始向後進行的。

原始碼分析之AbstractQueuedSynchronizer

acquire 獲取鎖

AQS自身僅定義了類似acquire方法。在實現鎖的時候,一般會實現一個繼承AQS的內部類Sync。而在Sync類中,我們根據需求來實現重寫tryAcquire方法和tryRelease方法。獨佔鎖acquire方法如下:

    public final void acquire(int arg) {

        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }複製程式碼
  • 通過tryAcquire(由不同的實現類實現)嘗試獲取鎖,如果可以獲取鎖直接返回。獲取不到鎖,則呼叫addWaiter方法;
    private Node addWaiter(Node mode) {
        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.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        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;
                }
            }
        }
    }複製程式碼
  • addWaiter方法作用是把當前執行緒封裝成Node節點,通過CAS操作快速嘗試掛載至佇列尾部。
    • 如果tail節點t已經有了:將t節點更新為當前節點node的前繼節點node.prev,將t.next更新為當前節點node
    • 如果tail節點新增失敗:
      • 如果tail節點為空,那麼原子化的分配一個頭節點,並將尾節點指向頭節點,這一步是初始化;
      • 如果tail節點不為空,迴圈重複addWaiter方法的工作直至當前節點入隊為止。
    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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }複製程式碼
  • 節點加入Sync Queue之後,接下來就是要進行鎖的獲取,或者說是訪問控制了,只有一個執行緒能夠在同一時刻繼續的執行,而其他的進入等待狀態。
    • 獲取當前節點的前繼節點
    • 當前繼節點是頭結點並且能夠獲取狀態,代表該當前節點佔有鎖;如果滿足上述條件,那麼代表能夠佔有鎖,根據節點對鎖佔有的含義,設定頭結點為當前節點。
    • 否則進入等待狀態。

至此,可以總結一次acquire的過程大致為:

原始碼分析之AbstractQueuedSynchronizer

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;
    }複製程式碼
  • 首先通過CAS操作變更同步狀態state
  • 釋放成功後,通過LockSupport.unpark方法來喚醒後繼節點,後繼節點繼續獲取鎖。

Condition Queue

AQS框架提供了一個ConditionObject內部類,給維護獨佔同步的類以及實現Lock介面的類使用。一個鎖物件可以關聯任意數目的條件物件,可以提供典型的Java監視器風格的awaitsignalsignalAll操作,包括帶有超時的,以及一些檢測、監控的方法。Condition Queue是普通的佇列並不要求是執行緒安全,原因是線上程在操作Condition時,要求執行緒必須獨佔鎖,不需要考慮併發的問題。

Condition Queue也是以Node為基礎的佇列。

        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;複製程式碼
await操作
  • Condition在執行await操作時,首先會呼叫addConditionWaiter()方法將當前執行緒封裝的Node節點加入到wait queue
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            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;
        }複製程式碼

上述addConditionWaiter的邏輯是:

  • 首先清除Condition Queue佇列中cancelled狀態的尾節點;
  • Condition Queue佇列為空,封裝當前執行緒的node節點為Condition QueuefirstWaiter。如Condition Queue佇列不為空,則把該節點加至佇列尾部。
 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }複製程式碼
  • 加入Condition queue之後,要釋放當前執行緒獲取的所有的鎖;
  • 如果執行緒沒有在Sync Queue中,將呼叫LockSupport.park阻塞當前執行緒,直到signalled或者interrupted喚醒去獲取鎖。
single 操作
        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }複製程式碼
  • 首先檢查執行緒是否獨佔鎖;
  • 獲取Codition QueuefirstWaiter,將節點轉移至Sync Queue中去。
singleAll 操作
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }複製程式碼

signalAll喚醒Condition Queue的所有等待執行緒,將所有的Condition Queue中的node元素轉移至Sync Queue中去。

其他API

這裡只介紹了獨佔鎖模式下,普通acquirerelease方法的原理,AQS還提供了很多可以供我們選擇的API

  • 如優先考慮中斷、超時的:acquireInterruptiblytryAcquireNanos
  • 如共享鎖模式下acquireSharedreleaseShared

等等...

這裡暫時不詳細分析,後面有時間的話可以再做了解。

參考:

相關文章