深入理解AbstractQueuedSynchronizer(AQS)

你聽___發表於2018-05-03

深入理解AbstractQueuedSynchronizer(AQS)

1. AQS簡介

上一篇文章中我們對lock和AbstractQueuedSynchronizer(AQS)有了初步的認識。在同步元件的實現中,AQS是核心部分,同步元件的實現者通過使用AQS提供的模板方法實現同步元件語義,AQS則實現了對同步狀態的管理,以及對阻塞執行緒進行排隊,等待通知等等一些底層的實現處理。AQS的核心也包括了這些方面:同步佇列,獨佔式鎖的獲取和釋放,共享鎖的獲取和釋放以及可中斷鎖,超時等待鎖獲取這些特性的實現,而這些實際上則是AQS提供出來的模板方法,歸納整理如下:

獨佔式鎖:

void acquire(int arg):獨佔式獲取同步狀態,如果獲取失敗則插入同步佇列進行等待; void acquireInterruptibly(int arg):與acquire方法相同,但在同步佇列中進行等待的時候可以檢測中斷; boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基礎上增加了超時等待功能,在超時時間內沒有獲得同步狀態返回false; boolean release(int arg):釋放同步狀態,該方法會喚醒在同步佇列中的下一個節點

共享式鎖:

void acquireShared(int arg):共享式獲取同步狀態,與獨佔式的區別在於同一時刻有多個執行緒獲取同步狀態; void acquireSharedInterruptibly(int arg):在acquireShared方法基礎上增加了能響應中斷的功能; boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基礎上增加了超時等待的功能; boolean releaseShared(int arg):共享式釋放同步狀態

要想掌握AQS的底層實現,其實也就是對這些模板方法的邏輯進行學習。在學習這些模板方法之前,我們得首先了解下AQS中的同步佇列是一種什麼樣的資料結構,因為同步佇列是AQS對同步狀態的管理的基石。

2. 同步佇列

當共享資源被某個執行緒佔有,其他請求該資源的執行緒將會阻塞,從而進入同步佇列。就資料結構而言,佇列的實現方式無外乎兩者一是通過陣列的形式,另外一種則是連結串列的形式。AQS中的同步佇列則是通過鏈式方式進行實現。接下來,很顯然我們至少會抱有這樣的疑問:**1. 節點的資料結構是什麼樣的?2. 是單向還是雙向?3. 是帶頭結點的還是不帶頭節點的?**我們依舊先是通過看原始碼的方式。

在AQS有一個靜態內部類Node,其中有這樣一些屬性:

volatile int waitStatus //節點狀態 volatile Node prev //當前節點/執行緒的前驅節點 volatile Node next; //當前節點/執行緒的後繼節點 volatile Thread thread;//加入同步佇列的執行緒引用 Node nextWaiter;//等待佇列中的下一個節點

節點的狀態有以下這些:

int CANCELLED = 1//節點從同步佇列中取消 int SIGNAL = -1//後繼節點的執行緒處於等待狀態,如果當前節點釋放同步狀態會通知後繼節點,使得後繼節點的執行緒能夠執行; int CONDITION = -2//當前節點進入等待佇列中 int PROPAGATE = -3//表示下一次共享式同步狀態獲取將會無條件傳播下去 int INITIAL = 0;//初始狀態

現在我們知道了節點的資料結構型別,並且每個節點擁有其前驅和後繼節點,很顯然這是一個雙向佇列。同樣的我們可以用一段demo看一下。

public class LockDemo {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
    }
}
複製程式碼

例項程式碼中開啟了5個執行緒,先獲取鎖之後再睡眠10S中,實際上這裡讓執行緒睡眠是想模擬出當執行緒無法獲取鎖時進入同步佇列的情況。通過debug,當Thread-4(在本例中最後一個執行緒)獲取鎖失敗後進入同步時,AQS時現在的同步佇列如圖所示:

LockDemo debug下 .png

Thread-0先獲得鎖後進行睡眠,其他執行緒(Thread-1,Thread-2,Thread-3,Thread-4)獲取鎖失敗進入同步佇列,同時也可以很清楚的看出來每個節點有兩個域:prev(前驅)和next(後繼),並且每個節點用來儲存獲取同步狀態失敗的執行緒引用以及等待狀態等資訊。另外AQS中有兩個重要的成員變數:

private transient volatile Node head;
private transient volatile Node tail;
複製程式碼

也就是說AQS實際上通過頭尾指標來管理同步佇列,同時實現包括獲取鎖失敗的執行緒進行入隊,釋放鎖時對同步佇列中的執行緒進行通知等核心方法。其示意圖如下:

佇列示意圖.png

通過對原始碼的理解以及做實驗的方式,現在我們可以清楚的知道這樣幾點:

  1. 節點的資料結構,即AQS的靜態內部類Node,節點的等待狀態等資訊
  2. 同步佇列是一個雙向佇列,AQS通過持有頭尾指標管理同步佇列

那麼,節點如何進行入隊和出隊是怎樣做的了?實際上這對應著鎖的獲取和釋放兩個操作:獲取鎖失敗進行入隊操作,獲取鎖成功進行出隊操作。

3. 獨佔鎖

3.1 獨佔鎖的獲取(acquire方法)

我們繼續通過看原始碼和debug的方式來看,還是以上面的demo為例,呼叫lock()方法是獲取獨佔式鎖,獲取失敗就將當前執行緒加入同步佇列,成功則執行緒執行。而lock()方法實際上會呼叫AQS的**acquire()**方法,原始碼如下

public final void acquire(int arg) {
		//先看同步狀態是否獲取成功,如果成功則方法結束返回
		//若失敗則先呼叫addWaiter()方法再呼叫acquireQueued()方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
複製程式碼

關鍵資訊請看註釋,acquire根據當前獲得同步狀態成功與否做了兩件事情:1. 成功,則方法結束返回,2. 失敗,則先呼叫addWaiter()然後在呼叫acquireQueued()方法。

獲取同步狀態失敗,入隊操作

當執行緒獲取獨佔式鎖失敗後就會將當前執行緒加入同步佇列,那麼加入佇列的方式是怎樣的了?我們接下來就應該去研究一下addWaiter()和acquireQueued()。addWaiter()原始碼如下:

private Node addWaiter(Node mode) {
		// 1. 將當前執行緒構建成Node型別
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 2. 當前尾節點是否為null?
		Node pred = tail;
        if (pred != null) {
			// 2.2 將當前節點尾插入的方式插入同步佇列中
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
		// 2.1. 當前同步佇列尾節點為null,說明當前執行緒是第一個加入同步佇列進行等待的執行緒
        enq(node);
        return node;
}
複製程式碼

分析可以看上面的註釋。程式的邏輯主要分為兩個部分:**1. 當前同步佇列的尾節點為null,呼叫方法enq()插入;2. 當前佇列的尾節點不為null,則採用尾插入(compareAndSetTail()方法)的方式入隊。**另外還會有另外一個問題:如果 if (compareAndSetTail(pred, node))為false怎麼辦?會繼續執行到enq()方法,同時很明顯compareAndSetTail是一個CAS操作,通常來說如果CAS操作失敗會繼續自旋(死迴圈)進行重試。因此,經過我們這樣的分析,enq()方法可能承擔兩個任務:**1. 處理當前同步佇列尾節點為null時進行入隊操作;2. 如果CAS尾插入節點失敗後負責自旋進行嘗試。**那麼是不是真的就像我們分析的一樣了?只有原始碼會告訴我們答案:),enq()原始碼如下:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
			if (t == null) { // Must initialize
				//1. 構造頭結點
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
				// 2. 尾插入,CAS操作失敗自旋嘗試
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}
複製程式碼

在上面的分析中我們可以看出在第1步中會先建立頭結點,說明同步佇列是帶頭結點的鏈式儲存結構。帶頭結點與不帶頭結點相比,會在入隊和出隊的操作中獲得更大的便捷性,因此同步佇列選擇了帶頭結點的鏈式儲存結構。那麼帶頭節點的佇列初始化時機是什麼?自然而然是在tail為null時,即當前執行緒是第一次插入同步佇列。compareAndSetTail(t, node)方法會利用CAS操作設定尾節點,如果CAS操作失敗會在for (;;)for死迴圈中不斷嘗試,直至成功return返回為止。因此,對enq()方法可以做這樣的總結:

  1. 在當前執行緒是第一個加入同步佇列時,呼叫compareAndSetHead(new Node())方法,完成鏈式佇列的頭結點的初始化
  2. 自旋不斷嘗試CAS尾插入節點直至成功為止

現在我們已經很清楚獲取獨佔式鎖失敗的執行緒包裝成Node然後插入同步佇列的過程了?那麼緊接著會有下一個問題?在同步佇列中的節點(執行緒)會做什麼事情了來保證自己能夠有機會獲得獨佔式鎖了?帶著這樣的問題我們就來看看acquireQueued()方法,從方法名就可以很清楚,這個方法的作用就是排隊獲取鎖的過程,原始碼如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
				// 1. 獲得當前節點的先驅節點
                final Node p = node.predecessor();
				// 2. 當前節點能否獲取獨佔式鎖					
				// 2.1 如果當前節點的先驅節點是頭結點並且成功獲取同步狀態,即可以獲得獨佔式鎖
                if (p == head && tryAcquire(arg)) {
					//佇列頭指標用指向當前節點
                    setHead(node);
					//釋放前驅節點
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
				// 2.2 獲取鎖失敗,執行緒進入等待狀態等待獲取獨佔式鎖
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}
複製程式碼

程式邏輯通過註釋已經標出,整體來看這是一個這又是一個自旋的過程(for (;;)),程式碼首先獲取當前節點的先驅節點,如果先驅節點是頭結點的並且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所指向的執行緒能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。整體示意圖為下圖:

自旋獲取鎖整體示意圖.png

獲取鎖成功,出隊操作

獲取鎖的節點出隊的邏輯是:

//佇列頭結點引用指向當前節點
setHead(node);
//釋放前驅節點
p.next = null; // help GC
failed = false;
return interrupted;
複製程式碼

setHead()方法為:

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
}
複製程式碼

將當前節點通過setHead()方法設定為佇列的頭結點,然後將之前的頭結點的next域設定為null並且pre域也為null,即與佇列斷開,無任何引用方便GC時能夠將記憶體進行回收。示意圖如下:

當前節點引用執行緒獲取鎖,當前節點設定為佇列頭結點.png

那麼當獲取鎖失敗的時候會呼叫shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他們做了什麼事情。shouldParkAfterFailedAcquire()方法原始碼為:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
複製程式碼

shouldParkAfterFailedAcquire()方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將節點狀態由INITIAL設定成SIGNAL,表示當前執行緒阻塞。當compareAndSetWaitStatus設定失敗則說明shouldParkAfterFailedAcquire方法返回false,然後會在acquireQueued()方法中for (;;)死迴圈中會繼續重試,直至compareAndSetWaitStatus設定節點狀態位為SIGNAL時shouldParkAfterFailedAcquire返回true時才會執行方法parkAndCheckInterrupt()方法,該方法的原始碼為:

private final boolean parkAndCheckInterrupt() {
        //使得該執行緒阻塞
		LockSupport.park(this);
        return Thread.interrupted();
}
複製程式碼

該方法的關鍵是會呼叫LookSupport.park()方法(關於LookSupport會在以後的文章進行討論),該方法是用來阻塞當前執行緒的。因此到這裡就應該清楚了,acquireQueued()在自旋過程中主要完成了兩件事情:

  1. 如果當前節點的前驅節點是頭節點,並且能夠獲得同步狀態的話,當前執行緒能夠獲得鎖該方法執行結束退出
  2. 獲取鎖失敗的話,先將節點狀態設定成SIGNAL,然後呼叫LookSupport.park方法使得當前執行緒阻塞

經過上面的分析,獨佔式鎖的獲取過程也就是acquire()方法的執行流程如下圖所示:

獨佔式鎖獲取(acquire()方法)流程圖.png

3.2 獨佔鎖的釋放(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返回true)則會執行if塊中的程式碼,當head指向的頭結點不為null,並且該節點的狀態值不為0的話才會執行unparkSuccessor()方法。unparkSuccessor方法原始碼:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */

	//頭節點的後繼節點
    Node s = node.next;
    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)
		//後繼節點不為null時喚醒該執行緒
        LockSupport.unpark(s.thread);
}
複製程式碼

原始碼的關鍵資訊請看註釋,首先獲取頭節點的後繼節點,當後繼節點的時候會呼叫LookSupport.unpark()方法,該方法會喚醒該節點的後繼節點所包裝的執行緒。因此,每一次鎖釋放後就會喚醒佇列中該節點的後繼節點所引用的執行緒,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。

到現在我們終於啃下了一塊硬骨頭了,通過學習原始碼的方式非常深刻的學習到了獨佔式鎖的獲取和釋放的過程以及同步佇列。可以做一下總結:

  1. 執行緒獲取鎖失敗,執行緒被封裝成Node進行入隊操作,核心方法在於addWaiter()和enq(),同時enq()完成對同步佇列的頭結點初始化工作以及CAS操作失敗的重試;
  2. 執行緒獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點並且成功獲得同步狀態時,節點出隊即該節點引用的執行緒獲得鎖,否則,當不滿足條件時就會呼叫LookSupport.park()方法使得執行緒阻塞
  3. 釋放鎖的時候會喚醒後繼節點;

總體來說:在獲取同步狀態時,AQS維護一個同步佇列,獲取同步狀態失敗的執行緒會加入到佇列中進行自旋;移除佇列(或停止自旋)的條件是前驅節點是頭結點並且成功獲得了同步狀態。在釋放同步狀態時,同步器會呼叫unparkSuccessor()方法喚醒後繼節點。

獨佔鎖特性學習

3.3 可中斷式獲取鎖(acquireInterruptibly方法)

我們知道lock相較於synchronized有一些更方便的特性,比如能響應中斷以及超時等待等特性,現在我們依舊採用通過學習原始碼的方式來看看能夠響應中斷是怎麼實現的。可響應中斷式鎖可呼叫方法lock.lockInterruptibly();而該方法其底層會呼叫AQS的acquireInterruptibly方法,原始碼為:

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
		//執行緒獲取鎖失敗
        doAcquireInterruptibly(arg);
}
複製程式碼

在獲取同步狀態失敗後就會呼叫doAcquireInterruptibly方法:

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
	//將節點插入到同步佇列中
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //獲取鎖出隊
			if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
				//執行緒中斷拋異常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

關鍵資訊請看註釋,現在看這段程式碼就很輕鬆了吧:),與acquire方法邏輯幾乎一致,唯一的區別是當parkAndCheckInterrupt返回true時即執行緒阻塞時該執行緒被中斷,程式碼丟擲被中斷異常。

3.4 超時等待式獲取鎖(tryAcquireNanos()方法)

通過呼叫lock.tryLock(timeout,TimeUnit)方式達到超時等待獲取鎖的效果,該方法會在三種情況下才會返回:

  1. 在超時時間內,當前執行緒成功獲取了鎖;
  2. 當前執行緒在超時時間內被中斷;
  3. 超時時間結束,仍未獲得鎖返回false。

我們仍然通過採取閱讀原始碼的方式來學習底層具體是怎麼實現的,該方法會呼叫AQS的方法tryAcquireNanos(),原始碼為:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
		//實現超時等待的效果
        doAcquireNanos(arg, nanosTimeout);
}
複製程式碼

很顯然這段原始碼最終是靠doAcquireNanos方法實現超時等待的效果,該方法原始碼如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
	//1. 根據超時時間和當前時間計算出截止時間
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
			//2. 當前執行緒獲得鎖出佇列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
			// 3.1 重新計算超時時間
            nanosTimeout = deadline - System.nanoTime();
            // 3.2 已經超時返回false
			if (nanosTimeout <= 0L)
                return false;
			// 3.3 執行緒阻塞等待 
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 3.4 執行緒被中斷丟擲被中斷異常
			if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

程式邏輯如圖所示:

超時等待式獲取鎖(doAcquireNanos()方法)

程式邏輯同獨佔鎖可響應中斷式獲取基本一致,唯一的不同在於獲取鎖失敗後,對超時時間的處理上,在第1步會先計算出按照現在時間和超時時間計算出理論上的截止時間,比如當前時間是8h10min,超時時間是10min,那麼根據deadline = System.nanoTime() + nanosTimeout計算出剛好達到超時時間時的系統時間就是8h 10min+10min = 8h 20min。然後根據deadline - System.nanoTime()就可以判斷是否已經超時了,比如,當前系統時間是8h 30min很明顯已經超過了理論上的系統時間8h 20min,deadline - System.nanoTime()計算出來就是一個負數,自然而然會在3.2步中的If判斷之間返回false。如果還沒有超時即3.2步中的if判斷為true時就會繼續執行3.3步通過LockSupport.parkNanos使得當前執行緒阻塞,同時在3.4步增加了對中斷的檢測,若檢測出被中斷直接丟擲被中斷異常。

4. 共享鎖

4.1 共享鎖的獲取(acquireShared()方法)

在聊完AQS對獨佔鎖的實現後,我們繼續一鼓作氣的來看看共享鎖是怎樣實現的?共享鎖的獲取方法為acquireShared,原始碼為:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
複製程式碼

這段原始碼的邏輯很容易理解,在該方法中會首先呼叫tryAcquireShared方法,tryAcquireShared返回值是一個int型別,當返回值為大於等於0的時候方法結束說明獲得成功獲取鎖,否則,表明獲取同步狀態失敗即所引用的執行緒獲取鎖失敗,會執行doAcquireShared方法,該方法的原始碼為:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
					// 當該節點的前驅節點是頭結點且成功獲取同步狀態
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

現在來看這段程式碼會不會很容易了?邏輯幾乎和獨佔式鎖的獲取一模一樣,這裡的自旋過程中能夠退出的條件是當前節點的前驅節點是頭結點並且tryAcquireShared(arg)返回值大於等於0即能成功獲得同步狀態

4.2 共享鎖的釋放(releaseShared()方法)

共享鎖的釋放在AQS中會呼叫方法releaseShared:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
複製程式碼

當成功釋放同步狀態之後即tryReleaseShared會繼續執行doReleaseShared方法:

private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
複製程式碼

這段方法跟獨佔式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對於能夠支援多個執行緒同時訪問的併發元件,必須保證多個執行緒能夠安全的釋放同步狀態,這裡採用的CAS保證,當CAS操作失敗continue,在下一次迴圈中進行重試。

4.3 可中斷(acquireSharedInterruptibly()方法),超時等待(tryAcquireSharedNanos()方法)

關於可中斷鎖以及超時等待的特性其實現和獨佔式鎖可中斷獲取鎖以及超時等待的實現幾乎一致,具體的就不再說了,如果理解了上面的內容對這部分的理解也是水到渠成的。

通過這篇,加深了對AQS的底層實現更加清楚了,也對了解併發元件的實現原理打下了基礎,學無止境,繼續加油:);如果覺得不錯,請給贊,嘿嘿。

參考文獻

《java併發程式設計的藝術》

相關文章