AQS:JAVA經典之鎖實現演算法(一)

秋褲Boy發表於2019-01-21

序言

AQS可以說是JAVA原始碼中必讀原始碼之一。同時它也是JAVA大廠面試的高頻知識點之一。認識並瞭解它,JAVA初中升高階工程師必備知識點之一。 AQS是AbstractQueuedSynchronizer的簡稱,它也是JUC包下眾多非原生鎖實現的核心。

一:AQS基礎概況

AQS是基於CLH佇列演算法改進實現的鎖機制。大體邏輯是AQS內部有一個鏈型佇列,佇列結點類是AQS的一個內部類Node,形成一個類似如下Sync Queue(記住這個名詞)

Sync Queue

可以看出,一個Node除了前後結點的索引外,還維護了一個Thread物件,一個int的waitStatus。 Thread物件就是處於競爭佇列中的執行緒物件本身。 waitStatus表示當前競爭結點的狀態,這裡暫且忽略掉。

處於隊首的,即Head所指向的結點,即為獲取到鎖的結點。釋放鎖即為出隊,後續結點則成為隊首,即獲取到鎖

Tips:這裡幫大家理解一個事情,每一個ReentrantLock例項都有且只有一個AQS例項,一個AQS例項維護一個Sync Queue。所以說,當我們的業務程式碼中的多個執行緒對同一個ReentrantLock例項進行鎖競爭操作時,其實際就是對同一個Sync Queue的佇列進行入隊、出隊操作。

二:AQS呼叫入口----ReentrantLock

我們在用ReentrantLock時,程式碼通常如下:

ReentrantLock lock = new ReentrantLock();
Runnable runnable = new Runnable() {
  public void run() {
    lock.lock();
    Sys.out(Thread.currentThread().name() + "搶到鎖");
    lock.unlock();
  }
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t2.start();
t1.start();
複製程式碼

可見執行緒t1,t2競爭lock這個鎖。 lock.lock()搶鎖 lock.unlock()釋放鎖 來看入口函式lock

public void lock() {
  sync.lock();
}
複製程式碼

syncReentrantLock內的一個繼承了AQS的抽象類

abstract static class Sync extends AbstractQueuedSynchronizer {
}
複製程式碼

抽象類的具體實現是另兩個內部類NonFairSync FairSync,分別代表非公平鎖、公平鎖。

我們系統來看下繼承圖

Sync父子圖

ReentrantLock中的sync例項,預設是在建構函式中初始化的,

public ReentrantLock() {
  sync = new NonfairSync();
}
複製程式碼

那我們就看預設的NonFairSync的實現邏輯。

NonFairSync

當我們呼叫ReentrantLock.lock()時,直接呼叫到的是NonFairSync.lock()

       /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
複製程式碼

compareAndSetState(0, 1)通過CAS(CAS是啥,這裡不講,參考樂觀鎖。具體Google吧)方式,設定AQS的int state欄位為1。AQS內部就是通過這個state是否為0來判斷當前鎖是否已經被執行緒獲取到。 返回true,則說明獲取鎖成功,設定當前鎖的獨佔執行緒setExclusiveOwnerThreaThread.currentThread()); 否則,acquire(1)嘗試獲d(取鎖。這個方法會自旋、阻塞,一直到獲取鎖成功為止。這裡,傳進去的引數1,就參考樂觀鎖的版本欄位,同時,這裡,它還記錄了可重入鎖重複獲取到鎖的次數。只有釋放同樣次數才能最終釋放鎖。

具體看AQS的acquire()的邏輯

AbstractQueuedSynchronizer.acquire()

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

要注意,這個方法是忽略執行緒中斷的。 先看tryAcquire(arg),這個方法是AQS留給子實現類的口子,具體實現看NonFairSync,它的實現裡直接呼叫了Sync.nonfairTryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製程式碼

可以看到方法內,先是嘗試獲取state = 0時的初始鎖,如果失敗,判斷當前鎖是否是被當前執行緒獲取,是的話,將acquire時傳入的引數累加到state欄位上。在這裡,這個欄位就是用來記錄重複獲取鎖的次數。 獲取失敗則返回false

回到acquire 在獲取失敗,返回false後,才會繼續呼叫 &&右邊的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。 先看addWaiter

private Node addWaiter(Node mode) {
1:        Node node = new Node(Thread.currentThread(), mode);
2:        // Try the fast path of enq; backup to full enq on failure
3:        Node pred = tail;
4:        if (pred != null) {
5:           node.prev = pred;
6:           if (compareAndSetTail(pred, node)) {
7:                pred.next = node;
8:                return node;
9:            }
10:        }
11:       enq(node);
12:        return node;
13:    }
複製程式碼

利用傳進來的Node.EXCLUSIVE表示的排斥鎖引數以及當前執行緒例項初始化新Node,第4-10行程式碼是在Sync Queue佇列內有競爭執行緒時進入。為空時會走到enq(node),這裡是在競爭為空時將競爭執行緒入隊的操作。 然後返回當前競爭的執行緒node 此時Sync Queue如圖

Sync Queue.png
我們假設node_1是已經獲取到鎖的結點,node_2即為我們當前操作的結點。

返回acquire接著看 addWaiter返回的node直接作為引數給了acquireQueued,這個方法就是主要的node競爭鎖方法。

final boolean acquireQueued(final Node node, int arg) {
        // 獲取鎖成功失敗標記
        boolean failed = true;
        try {
            // 當前競爭執行緒的中斷標記
            boolean interrupted = false;
            // 自旋競爭鎖,競爭不到鎖的話,執行緒又沒有中斷
            // 則一直在這兒迴圈
            for (;;) {
                // 獲取當前執行緒的前驅結點
                final Node p = node.predecessor();
                // 如果前驅結點是頭結點,則嘗試去tryAcquire
                // 這個邏輯我們之前看過,當前執行緒未獲取到鎖的情況下
                // 在AQS的state欄位不為0時,則返回false
                if (p == head && tryAcquire(arg)) {
                    // 進入到這裡,說明要不就是當前執行緒在重複獲取
                    // 要不就是前邊的結點釋放鎖,state 歸0,這裡獲取到
                    setHead(node);
                    p.next = null; // help GC
                    // 標識競爭鎖成功
                    failed = false;
                    // 這個方法不響應執行緒中斷,但是會返回執行緒在競爭鎖過程中
                    // 中斷標記返回
                    return interrupted;
                }
                // 若獲取鎖失敗,則到這裡。這裡的邏輯主要在If判斷
                // 的兩個方法中,用來將當前執行緒掛起的,具體邏輯
                // 看下面
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製程式碼

park就是停下的意思。所以這個方法從名字上也比較好理解,就是 掛起執行緒並且檢查執行緒的中斷狀態。這裡要注意,LockSupport.part(this)方法是會線上程中斷時自動喚醒的

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
複製程式碼

這個方法傳入了當前競爭結點及其前驅結點

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前驅結點的等待狀態。這裡只需要記住,我們這裡考慮的是
        // 非共享鎖、非公平鎖的AQS。所以,只需要確保當前競爭
        // 結點的前驅結點狀態為SIGNAL就好。剩下的狀態,
        // 與我們此時研究的情況而言沒有用
        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 {
            /*
             * 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.
             */
            // 如果前驅結點的status為0,則將其改為SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
複製程式碼

shouldParkAfterFailedAcquire可以看出來,在確保前驅結點status為SIGNAL時,就可以放心的去unsafe.park()了。之所以要為SIGNAL,是因為這個狀態含義為:當前結點OVER時要喚醒後繼結點。

所以不難推出,我們的結點現在就park在那了。等他前驅結點釋放鎖,或者自己interrupt來喚醒,但因為這個方法是無視中斷的,所以即使interrupt了,只是設定了一個標記位,但仍然在迴圈中。


這裡假設前驅結點獲取鎖後釋放,則當前結點在parkAndCheckInterrupt()方法中被喚醒,而後再次迴圈for(;;),這次會在第一個if中就進入,當前結點獲取到鎖,然後重置Head指向的結點等,返回當前執行緒的中斷標記。

返回acquire

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

if中的selfInterrupt()方法只是去重新設定當前執行緒的中斷標記位。這是因為獲取執行緒中斷狀態的方法,在返回狀態欄位的同時,也會重置欄位,所以需要標記後重新設定相應的值。

下面我們看下AQS釋放鎖的介面方法

ReentrantLock.unlock

    public void unlock() {
        sync.release(1);
    }
複製程式碼

追進去

ReentrantLock.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;
    }
複製程式碼

tryRelease是在NonFairLock中的實現的,如果是釋放成功,則在Head存在並且狀態不為0(其實可以理解為值為SIGNAL時)去喚醒Head的後繼結點。

NonFailLock.tryRelease

下面看下NonFairLocktryRelease方法的實現

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
複製程式碼

可以看出,這個tryRelease其實就是去判斷下是不是當前執行緒擁有鎖,是的話,判斷下當前的釋放鎖是否完全釋放,因為鎖可以重複獲取,完全釋放的話,就設定state為0,代表AQS的鎖已經被釋放了。

相關文章