AQS源碼分析(以ReentrantLock爲例)

前言

當我們在Java併發編程中,可能會時常使用ReentrantLockSemaphoreCountDwonLatch等同步工具保證線程安全,但是我們可能對它們到底是如何保證線程安全並不是很清楚。本篇文章就通過詳細解析他們的共同依賴類AbstractQueuedSynchronizer來探究它們是如何實現對資源的控制的。爲了便於分析,本篇文章主要從ReentrantLock爲切入點,閱讀本文之後你們可以自行分析其他實現類的實現邏輯。

案例

首先,我們來回顧一下ReentrantLock的一個使用案例:

public class Bootstrap {
    private static final Lock lock = new ReentrantLock();
    private int count = 0;
    public void count() {
        try {
            lock.lock();
            count++;
        } finally {
            lock.unlock();
        }
    }
}

這個demo很簡單,就是對count進行計數,並且通過ReentrantLock保證多線程環境下的數據安全問題。

源碼分析

初始化

上面的代碼首先通過ReentrantLock構造函數創建了一個Lock對象,我們來看一下實際創建了哪些對象。

    public ReentrantLock() {
        sync = new NonfairSync();
    }

從字面就可以看出創建了一個非公平鎖對象,不過相信大家之前就知道ReentrantLock默認支持非公平鎖,但是同樣也支持公平鎖,因此如果你通過new ReentrantLock(true)就可以創建一個公平鎖對象,具體源碼如下:

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

到這邊我們知道創建鎖的兩種方式,接下來通過具體的代碼看一下這兩種鎖有什麼區別:

公平鎖實現部分源碼

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        ...
    }

非公平鎖實現部分源碼:

 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        ...
    }

可以看見非公平鎖多了一個判斷條件,現在我暫時告訴你這個條件是讓當前線程嘗試去獲取鎖,如果獲取成功則直接將它設置成獨佔鎖模式。這也正好體現了非公平的原則,直接嘗試獲取鎖,不成功再去和別的線程一樣等待。

由於NonfairSync的邏輯包含了FairSync,因此接下來就從NonfairSync分析下去,在此之前看一下NonfairSync繼承圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-DYak23Cn-1585575769927)(https://s1.ax1x.com/2020/03/26/Gpl0sO.png)]

可以看到核心類Sync繼承了AQS,NonfairSync和FairSync都繼承了Sync。通過這一套繼承機制實現了ReentrantLock的加鎖和釋放鎖的邏輯,從這裏就可以看出,AQS並沒有具體的實現邏輯,它只是規定了管理線程等待和獲取鎖的機制(這是下面要講解的重點),而ReentrantLock(類似的其他實現類像Semaphore等)實現了具體該何時獲取鎖,何時釋放鎖的邏輯。

AQS源碼分析

在講解AQS之前,我先用一張圖來描述一下它的運作原理。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xQLxYmBI-1585575769929)(https://s1.ax1x.com/2020/03/30/GuSKbD.png)]

當一個線程沒有獲取到鎖的時候,AQS會通過內部的Node類將Thread封裝成一個節點,並且通過雙向鏈表的形式在等待鎖的過程中將節點加入到隊列中去(這邊使用CLH隊列的一個變種,有興趣的可以自己去了解一下什麼是CLH隊列)。


接下來讓我們從頭開始探索一下NonfairSync完整的運作機制:

compareAndSetState

當案例中調用lock.lock()方法時,就會進入到NonfairSync的lock()方法中,第一步是通過compareAndSetState(0, 1)來獲取鎖,我們看一下具體的代碼:

位於AQS類中:

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

可以看到,通過Unsafe類直接操作內存,實現CAS方式來更新state值,這邊引出了state的這個屬性,它是AQS的一個volatile類型的整數值,通過該值表示如果是0那麼鎖沒有被佔用,如果是1表示鎖已經被佔用。所以非公平鎖先調用這個方法去獲取鎖看看是否能夠成功,如果成功了就會進入setExclusiveOwnerThread(Thread.currentThread());方法,我們再來看一下這個方法的代碼

setExclusiveOwnerThread

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

這個方法存在於AbstractOwnableSynchronizer類中,通過上面的繼承圖可以看到它是AQS的父類。這個方法將exclusiveOwnerThread設置成了我們制定的線程,表示該線程正在佔有鎖。到這邊第一個條件結束了,表示如果能夠直接獲取到鎖,那麼就將當前線程鎖定,流程就結束了。


acquire

如果上面的條件不成立,那麼就會進入acquire(1)這個方法去獲取鎖,我們先看一下這個方法的代碼:

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

我們可以看到,主要涉及了兩個條件,接下來就逐一對它們進行說明:

tryAcquire

這個方法在AQS中沒有具體實現,這也是我們實現AQS需要自定義的方法。NonfairSync對應的實現如下:

     final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); 
      #1    int c = getState(); 
      #2    if (c == 0) {
               if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
      #3    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;
        }

這邊我對主要步驟進行編號後再講解:

首先是獲取到當前線程對象,然後通過getState()方法獲取到當前鎖的狀態(#1);

如果狀態是0,表示鎖還沒有被佔用,嘗試直接獲取鎖(這一步其實就是第一個條件做的事),獲取成功將當前線程設置成獨佔鎖模式(#2);

如果當前線程已經是獨佔鎖模式了,表示不用再次獲取鎖,直接將state進行累加就行不過需要注意state溢出,最後設置好新的state即可(這個條件其實就表明了ReentrantLock是可重入鎖)(#3);

如果上面的條件都不滿足就返回false。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

當直接獲取鎖不成功的時候,那麼就要將線程加入到隊列等待,對於如何將它加入隊列就涉及到了addWaiter(Node.EXCLUSIVE)方法,接下來先看一下它的具體實現代碼:

    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;
    }

可以看到首先會通過Node的初始化方法將Thread進行封裝,並且設置了節點的類型,這邊是EXCLUSIVE;

對於整個鏈表會有head節點和tail節點的概念,分別指向了鏈表的頭和尾。新加入的節點首先通過tail節點來快速進行設置,tail不爲空那麼直接將node的prev指向該節點並且把新的tail節點指向node節點;

如果失敗了就要執行enq(node)方法,重新進行入隊操作,enq方法的具體實現如下:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
     #1     if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

可以看見這是一個自旋的操作,首先會判斷tail節點是否爲空,如果是空的代表鏈表還沒有節點,那麼會初始化一個空節點並把tail和head節點都指向它(#1);

如果tail存在了的話那麼還是通過上面的方式將節點設置在鏈表尾部;

到這邊就可以看出,node的入隊操作是依賴於舊的tail節點,通過新節點的前置節點來設置新的tail節點。

acquireQueued

入隊操作完成後,我們需要進一步探究Node在隊列中是如何獲取鎖的,接下來我們看一下acquireQueued方法的具體實現:

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
   #1           if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
   #2           if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

首先兩個屬性我們需要知道,failed表示獲取鎖是否失敗,interupted表示線程是否被中斷過;

此外這個方法是一個自旋的操作,主要包括兩個步驟:

判斷當前節點的前驅節點是否是頭結點,如果是的話則再次嘗試獲取鎖,獲取成功就把新的頭結點設置成當前節點,然後返回interrupted;(#1)

如果當前節點的前驅節點不是頭結點或者獲取鎖失敗的話,那麼就需要判斷是否需要將當前線程進行阻塞,這邊涉及到兩個判斷條件shouldParkAfterFailedAcquireparkAndCheckInterrupt(),下面依次進行講解:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
    #1  if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
    #2  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;
    #3  } 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.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

這個方法主要針對當前節點,去判斷截取節點的狀態,如果waitStatus是SIGNAL直接返回true(#1);

如果waitStatus是大於0,表示前驅節點是取消狀態,那麼直接去獲取之前的節點知道前驅節點的waiteStatus不是大於0(#2);

如果waitStatus是0或者3,那麼直接將前驅節點的waitStatus設置爲SIGNAL(#3)。

通過這種方式來決定當前節點的線程是否需要阻塞。

如果是true那麼需要調用parkAndCheckInterrupt來實現阻塞,我們看一下具體實現:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可以看到使用了LockSupport類的park方法來進行線程的阻塞,該操作也依賴於Unsafe方法,直接操作具體的線程對應的permit執照來決定是否需要阻塞1。最後返回了線程的阻塞標識。

最後我們再回到acquire方法,線程阻塞過,需要執行selfInterrupt方法這是對中斷機制的一個響應操作。需要注意的是,如果是
lockInterruptibly方法,就是在阻塞操作有不一樣的地方,上面的是將interrupted設置爲了true,但是lockInterruptibly方法是直接throw new InterruptedException();中斷操作了,並且在finally中會去取消獲取鎖。

總結

最後做個總結,ReentrantLock的實現是依賴於AQS的,ReentrantLock層面只是重寫了獲取鎖的邏輯上的代碼。並且AQS是通過一個雙向隊列來存儲包含Thread信息的Node節點,並且通過state變量來表示鎖的獲取狀態。線程通過CAS方式設置鎖的狀態,通過節點的等待信息來決定是否去獲取鎖還是去阻塞線程。相信通過本篇文章大家也對AQS的實現原理有了一個直觀的瞭解

參考

轉載請註明出處!

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