AQS之ReentrantLock源碼解析

前言:

Java中的同步類ReentrantLock是基於AbstractQueuedSynchronizer(簡稱爲AQS)實現的。

今天從源碼來了解下ReentrantLock中非公平鎖的加鎖和釋放鎖(ReentrantLock中支持公平鎖和非公平鎖,默認是非公平鎖的,但可以通過創建ReentrantLock對象時傳入參數指定使用公平鎖)。

在瞭解ReentrantLock前,需要對AQS有一定的瞭解,否則在學習時會比較困難的,並且在通過源碼學習ReentrantLock時也會穿插着講解AQS內容。

AQS掃蕩:

1.0、AQS中state變量

​ AQS中提供了一個int類型的state變量,並且state變量被volatile修飾,表示state變量的讀寫操作可以保證原子性;並且AQS還提供了針對state變量的讀寫方法,以及使用CAS算法更新state變量的方法。 AQS使用state變量這個狀態變量來實現同步狀態。

①、源碼展示

/**
 * The synchronization state. 
 */
private volatile int state;

/**
 * get 獲取state變量值 
 */
protected final int getState() {
    return state;
}

/**
 * set 更新state變量值 
 * @param newState  新的狀態變量值
 */
protected final void setState(int newState) {
    state = newState;
}


/**
 * 使用CAS算法更新state變量值; 當從共享內存中讀取出的state變量值與expect期望值一致的話,
 * 就將其更新爲update值。使用CAS算法保證其操作的原子性
 *
 * @param expect  期望值
 * @param update  更新值
 */
protected final boolean compareAndSetState(int expect, int update) {
    // 使用Unsafe類的本地方法來實現CAS
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

1.1、state同步狀態的競爭

​ 多個線程同時競爭AQS的state同步狀態,在同一時刻只能有一個線程獲取到同步狀態(獲取到鎖),那其它沒獲取到鎖的線程該怎麼辦呢

它們會進去到一個同步隊列中,在隊列中等待同步鎖的釋放;

這個同步隊列是一個基於鏈表的雙向隊列 , 基於鏈表的話,就會存在Node節點,那麼AQS中節點是怎麼實現的呢

①、Node節點:

AQS中自己實現了一個內部Node節點類,Node節點類中定義了一些屬性,下面來簡單說說屬性的意思:

static final class Node {
        // 標誌在同步隊列中Node節點的模式,共享模式 
        static final Node SHARED = new Node();
        // 標誌在同步隊列中Node節點的模式,獨佔(排他)模式 
        static final Node EXCLUSIVE = null;

        // waitStatus值爲1時表示該線程節點已釋放(超時等),已取消的節點不會再阻塞。 
        static final int CANCELLED =  1;
    
        // waitStatus值爲-1時表示當此節點的前驅結點釋放鎖時,然後當前節點中的線程就可以去獲取鎖運行 
        static final int SIGNAL    = -1;
    
        /**
         * waitStatus爲-2時,表示該線程在condition隊列中阻塞(Condition有使用),
         * 當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從
         * 等待隊列轉移到同步隊列中,等待獲取同步鎖。
         */ 
        static final int CONDITION = -2;
    
        /**
         * waitStatus爲-3時,與共享模式有關,在共享模式下,該狀態表示可運行
         * (CountDownLatch中有使用)。
         */
        static final int PROPAGATE = -3;

        /**
         * waitStatus:等待狀態,指的是當前Node節點中存放的線程的等待狀態,
         * 等待狀態值就是上面的四個狀態值:CANCELLED、SIGNAL、CONDITION、PROPAGATE
         */
        volatile int waitStatus;

        /**
         * 因爲同步隊列是雙向隊列,那麼每個節點都會有指向前一個節點的 prev 指針
         */
        volatile Node prev;

        /**
         * 因爲同步隊列是雙向隊列,那麼每個節點也都會有指向後一個節點的 next 指針
         */
        volatile Node next;

        /**
         * Node節點中存放的阻塞的線程引用
         */
        volatile Thread thread;

        /**
         * 當前節點與其next後繼結點的所屬模式,是SHARED共享模式,還是EXCLUSIVE獨佔模式,
         *
         * 注:比如說當前節點A是共享的,那麼它的這個字段是shared,也就是說在這個等待隊列中,
         * A節點的後繼節點也是shared。
         */
        Node nextWaiter;

        /**
         * 獲取當前節點是否爲共享模式
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * 獲取當前節點的 prev前驅結點
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() { }
    
        // 在後面的addWaiter方法會使用到,線程競爭state同步鎖失敗時,會創建Node節點存放thread
        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

②、同步隊列結構圖(雙向隊列):

1.2、圖解AQS原理

​ 通過前面兩點,可以瞭解到AQS的原理到底是什麼了,總結爲一句話:AQS使用一個Volatile的int類型的成員變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取的排隊工作,通過CAS完成對State值的修改。

然後再來一張圖,使得理解更加深刻:

圖片來源:Java技術之AQS詳解

好了,AQS暫時可以先了解到這裏了,知道這些後,在後面瞭解ReentrantLock時就會變的容易些,並且後面通過源碼學習ReentrantLock時,由於會使用到AQS的模版方法,所以也會講解到AQS的內容。

劍指ReentrantLock源碼:

2.0、ReentrantLock vs Synchronized

​ 在瞭解ReentrantLock之前,先將ReentrantLockSynchronized進行比較下,這樣可以更加了解ReentrantLock的特性,也有助於下面源碼的閱讀;

2.1、ReentrantLock的公平鎖與非公平鎖

創建一個ReentrantLock對象,在創建對象時,如果不指定公平鎖的話,默認是非公平鎖;

①、簡單瞭解下什麼是公平鎖,什麼是非公平鎖?

公平鎖:按照申請同步鎖的順序來獲取鎖;

非公平鎖:不會按照申請鎖的順序獲取鎖,存在鎖的搶佔;

注:後面會通過源碼瞭解下非公平鎖和公平鎖是怎樣獲取鎖的。

②、源碼如下:

// 默認是非公平的鎖
ReentrantLock lock = new ReentrantLock();
// 構造方法默認創建了一個 NonfairSync 非公平鎖對象
public ReentrantLock() {
    // NonfairSync繼承了Sync類,Sync類又繼承了AQS類
    sync = new NonfairSync();
}


// 傳入參數 true,指定爲公平鎖
ReentrantLock lock = new ReentrantLock(true);
// 傳入參數的構造方法,當fair爲true時,創建一個公平鎖對象,否則創建一個非公平鎖對象
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2.2、通過源碼看下非公平鎖的加鎖機制:(獨佔模式)

①、開始先通過一個簡單流程圖來看下獨佔模式下加鎖的流程:

​ 圖片來源:美團技術團隊

②、源碼分析:加鎖時首先使用CAS算法嘗試將state狀態變量設置爲1,設置成功後,表示當前線程獲取到了鎖,然後將獨佔鎖的擁有者設置爲當前線程;如果CAS設置不成功,則進入Acquire方法進行後續處理。

final void lock() {
    // 使用CAS算法嘗試將state狀態變量設置爲1
    if (compareAndSetState(0, 1))
        // 設置成功後,表示當前線程獲取到了鎖,然後將獨佔鎖的擁有者設置爲當前線程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 進行後續處理,會涉及到重入性、創建Node節點加入到隊列尾等
        acquire(1);
}

③、探究下acquire(1) 方法裏面是什麼呢 acquire(1) 方法是AQS提供的方法:

public final void acquire(int arg) {
    /**
     * 使用tryAcquire()方法,讓當前線程嘗試獲取同步鎖,獲取成的話,就不會執行後面的acquireQueued()
     * 方法了,這是由於 && 邏輯運算符的特性決定的。
     *
     * 如果使用tryAcquire()方法獲取同步鎖失敗的話,就會繼續執行acquireQueued()方法,它的作用是
     * 一直死循環遍歷同步隊列,直到使addWaiter()方法創建的節點中線程獲取到鎖。
     *
     * 如果acquireQueued()返回的true,這個true不是代表成功的獲取到鎖,而是代表當前線程是否存在
     * 中斷標誌,如果存在的話,在獲取到同步鎖後,需要使用selfInterrupt()對當前線程進行中斷。
     */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

1)tryAcquire(arg) 方法源碼解讀:NonfairSync 非公平鎖中重寫了AQS的tryAcquire()方法

final boolean nonfairTryAcquire(int acquires) {
    // 當前線程
    final Thread current = Thread.currentThread();
    // 獲取當前state同步狀態變量值,由於使用volatile修飾,單獨的讀寫操作具有原子性
    int c = getState();
    // 如果狀態值爲0
    if (c == 0) {
        // 使用compareAndSetState方法這個CAS算法嘗試將state同步狀態變量設置爲1 獲取同步鎖
        if (compareAndSetState(0, acquires)) {
            // 然後將獨佔鎖的擁有者設置爲當前線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果擁有獨佔鎖的的線程是當前線程的話,表示當前線程需要重複獲取鎖(重入鎖)
    else if (current == getExclusiveOwnerThread()) {
        // 當前同步狀態state變量值加1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 寫入state同步狀態變量值,由於使用volatile修飾,單獨的讀寫操作具有原子性
        setState(nextc);
        return true;
    }
    return false;
}

2)addWaiter( Node.EXCLUSIVE ) :創建一個同步隊列Node節點,同時綁定節點的模式爲獨佔模式,並且將創建的節點插入到同步隊列尾部;addWaiter( ) 方法是AQS提供方法。

private Node addWaiter(Node mode) {
    // model參數是獨佔模式,默認爲null;
    Node node = new Node(Thread.currentThread(), mode);
    // 將當前同步隊列的tail尾節點的地址引用賦值給pre變量
    Node pred = tail;
    // 如果pre不爲null,說明同步隊列中存在節點
    if (pred != null) {
        // 當前節點的前驅結點指向pre尾節點
        node.prev = pred;
        // 使用CAS算法將當前節點設置爲尾節點,使用CAS保證其原子性
        if (compareAndSetTail(pred, node)) {
            // 尾節點設置成功,將pre舊尾節點的後繼結點指向新尾節點node
            pred.next = node;
            return node;
        }
    }
    // 如果尾節點爲null,表示同步隊列中還沒有節點,enq()方法將當前node節點插入到隊列中
    enq(node);
    return node;
}

3)、說完addWaiter( Node.EXCLUSIVE )方法,接下來說下**acquireQueued()**方法,它是怎樣使addWaiter()創建的節點中的線程獲取到state同步鎖的。(這個方法也是AQS提供的)

源碼走起:

final boolean acquireQueued(final Node node, int arg) {
    // 標誌cancelAcquire()方法是否執行
    boolean failed = true;
    try {
        // 標誌是否中斷,默認爲false不中斷
        boolean interrupted = false;
        for (;;) {
            // 獲取當前節點的前驅結點
            final Node p = node.predecessor();
            /**
             * 如果當前節點的前驅結點已經是同步隊列的頭結點了,說明了兩點內容:
             * 1、其前驅結點已經獲取到了同步鎖了,並且鎖還沒釋放
             * 2、其前驅結點已經獲取到了同步鎖了,但是鎖已經釋放了
             *
             * 然後使用tryAcquire()方法去嘗試獲取同步鎖,如果前驅結點已經釋放了鎖,那麼就會獲取成功,
             * 否則同步鎖獲取失敗,繼續循環
             */
            if (p == head && tryAcquire(arg)) {
                // 將當前節點設置爲同步隊列的head頭結點
                setHead(node);
                // 然後將當前節點的前驅結點的後繼結點置爲null,幫助進行垃圾回收
                p.next = null; // help GC
                failed = false;
                // 返回中斷的標誌
                return interrupted;
            }
            /**
             * shouldParkAfterFailedAcquire()是對當前節點的前驅結點的狀態進行判斷,以及去針對各種
             * 狀態做出相應處理,由於文章篇幅問題,具體源碼本文不做講解;只需知道如果前驅結點p的狀態爲
             * SIGNAL的話,就返回true。
             *
             * parkAndCheckInterrupt()方法會使當前線程進去waiting狀態,並且查看當前線程是否被中斷,
             * interrupted() 同時會將中斷標誌清除。
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 中斷標誌置爲true
                interrupted = true;
        }
    } finally {
        if (failed)
            /**
             * 如果for(;;)循環中出現異常,並且failed=false沒有執行的話,cancelAcquire方法
             * 就會將當前線程的狀態置爲 node.CANCELLED 已取消狀態,並且將當前節點node移出
             * 同步隊列。
             */
            cancelAcquire(node);
    }
}

4)、最後說下 selfInterrupt() 方法, 這個方法就是將當前線程進行中斷:

static void selfInterrupt() {
    // 中斷當前線程
    Thread.currentThread().interrupt();
}

2.3、公平鎖與非公平鎖在加鎖時的區別:

①、公平鎖 FairSync 的加鎖 lock() 加鎖方法:

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

②、非公平鎖 NonfairSync 的加鎖 lock() 加鎖方法:上面講解源碼的時候有提到喲,還有印象嗎,沒印象的話也沒關係,不要哭 , 嘿嘿,我都準備好了。 源碼奉上:

final void lock() {
    /** 
     * 看到這,是不是發現了什麼,非公平鎖在此處直觀看的話,發現比公平鎖多了這幾行代碼; 
     * 這裏就是使得線程存在了一個搶佔,如果當前同步隊列中的head頭結點中 線程A 剛好釋放了同步鎖,
     * 然後此時 線程B 正好來了,那麼此時線程B就會獲取到鎖,而此時同步隊列中head頭結點的後繼結點中的
     * 線程C 就無法獲取到同步鎖,只能等待線程B釋放鎖後,嘗試獲取鎖了。
     */
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

③、除了上面那處不同之外,還有別的地方嗎;別急,再看看 acquire(1) 方法是否一樣呢?

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

​ 誒呀,方法點進去都是一樣的呀,可不嘛,都是調用的AQS提供的 acquire(1) 方法;但是彆着急,上面在講解非公平鎖加鎖時,有提到的 tryAcquire(arg) 方法在AQS的不同子孫類中都有各自的實現的。現在打開公平鎖的 tryAcquire(arg) 方法看看其源碼與非公平鎖有什麼區別:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        /**
         * 通過對比源碼發現,公平鎖比非公平鎖多了這塊代碼: !hasQueuedPredecessors() 
         * hasQueuedPredecessors() 是做什麼呢?就是判斷當前同步隊列中是否存在節點,如果存在節點呢,
         * 就返回true,由於前面有個 !,那麼就是false,再根據 && 邏輯運算符的特性,不會繼續執行了;
         * 
         * tryAcquire()方法直接返回false,後面的邏輯就和非公平鎖的一致了,就是創建Node節點,並將
         * 節點加入到同步隊列尾; 公平鎖:發現當前同步隊列中存在節點,有線程在自己前面已經申請可鎖,那
         * 自己就得乖乖的向後面排隊去。
         *
         * 友情提示:在生活中,我們也需要按照先來後到去排隊,保證素質; 還有就是怕你們不排隊被別人打了。
         */
        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;
}

鬆口氣,從中午一直寫到下午快四點了,先讓我歇口氣,快累成狗了;本文還剩下釋放鎖部分沒寫呢,歇口氣,喝口水繼續

注意:ReentrantLock在釋放鎖的時候,並不區分公平鎖和非公平鎖

2.4、通過源碼看下釋放鎖機制:(獨佔模式)

①、unlock() 釋放鎖的方法:

public void unlock() {
    // 釋放鎖時,需要將state同步狀態變量值進行減 1,傳入參數 1
    sync.release(1);
}

②、release( int arg ) 方法解析:(此方法是AQS提供的)

public final boolean release(int arg) {
    // tryRelease方法:嘗試釋放鎖,成功true,失敗false
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 頭結點不爲空並且頭結點的waitStatus不是初始化節點情況,然後喚醒此阻塞的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

注意:這裏的判斷條件爲什麼是h != null && h.waitStatus != 0?

h == null Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節點。所以說,這裏如果還沒來得及入隊,就會出現head == null 的情況。

h != null && waitStatus == 0 表明後繼節點對應的線程仍在運行中,不需要喚醒。

h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒。

③、然後再來看看tryRelease(arg) 方法:

protected final boolean tryRelease(int releases) {
    // 當前state狀態值進行減一
    int c = getState() - releases;
    // 如果當前獨佔鎖的擁有者不是當前線程,則拋出 非法監視器狀態 異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新state同步狀態值
    setState(c);
    return free;
}

④、最後看看unparkSuccessor(Node node) 方法:

private void unparkSuccessor(Node node) {
	// 獲取頭結點waitStatus
	int ws = node.waitStatus;
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0);
	// 獲取當前節點的下一個節點
	Node s = node.next;
	// 如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled狀態的節點
	if (s == null || s.waitStatus > 0) {
		s = null;
		// 就從尾部節點開始找,到隊首,找到隊列第一個waitStatus<0的節點。
		for (Node t = tail; t != null && t != node; t = t.prev)
			if (t.waitStatus <= 0)
				s = t;
	}
	// 如果當前節點的後繼結點不爲null,則將其節點中處於阻塞狀態的線程unpark喚醒
	if (s != null)
		LockSupport.unpark(s.thread);
}

注意:爲什麼要從後往前找第一個非Cancelled的節點呢?原因如下:

由於之前加鎖時的**addWaiter( )**方法的原因;

private Node addWaiter(Node mode) {
    // model參數是獨佔模式,默認爲null;
    Node node = new Node(Thread.currentThread(), mode);
    // 將當前同步隊列的tail尾節點的地址引用賦值給pre變量
    Node pred = tail;
    // 如果pre不爲null,說明同步隊列中存在節點
    if (pred != null) {
        // 當前節點的前驅結點指向pre尾節點
        node.prev = pred;
        // 使用CAS算法將當前節點設置爲尾節點,使用CAS保證其原子性
        if (compareAndSetTail(pred, node)) {
            // 尾節點設置成功,將pre舊尾節點的後繼結點指向新尾節點node
            pred.next = node;
            return node;
        }
    }
    // 如果尾節點爲null,表示同步隊列中還沒有節點,enq()方法將當前node節點插入到隊列中
    enq(node);
    return node;
}

從這裏可以看到,節點入隊並不是原子操作,也就是說,node.prev = pred ; compareAndSetTail( pred, node ) 這兩個地方可以看作Tail入隊的原子操作,但是此時 pred.next = node; 還沒執行,如果這個時候執行了unparkSuccessor方法,就沒辦法從前往後找了,所以需要從後往前找。還有一點原因,在產生CANCELLED狀態節點的時候,先斷開的是Next指針,Prev指針並未斷開,因此也是必須要從後往前遍歷才能夠遍歷完全部的Node。

end! 長吸一口氣,終於本文算是寫完了,最後再看看有沒有錯別字,以及排排版。

後續還會出一篇結合CountDownLatch源碼學習共享鎖(共享模式)的文章。


謝謝大家閱讀,鑑於本人水平有限,如有問題敬請提出。

參考資料:

1、從ReentrantLock的實現看AQS的原理及應用

2、Java技術之AQS詳解

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