Java併發程式設計之鎖機制之AQS

AndyandJennifer發表於2018-10-11

該文章屬於《Java併發程式設計》系列文章,如果想了解更多,請點選《Java併發程式設計之總目錄》

前言

在上篇文章《Java併發程式設計之鎖機制之Lock介面》中,我們已經瞭解了,Java下整個Lock介面下實現的鎖機制是通過AQS(這裡我們將AbstractQueuedSynchronizer 或AbstractQueuedLongSynchronizer統稱為AQS)與Condition來實現的。那下面我們就來具體瞭解AQS的內部細節與實現原理。

PS:該篇文章會以AbstractQueuedSynchronizer來進行講解,對AbstractQueuedLongSynchronizer有興趣的小夥伴,可以自行檢視相關資料。

AQS簡介

抽象佇列同步器AbstractQueuedSynchronizer (以下都簡稱AQS),是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變數來表示同步狀態,通過內建的FIFO(first-in-first-out)同步佇列來控制獲取共享資源的執行緒。

該類被設計為大多數同步元件的基類,這些同步元件都依賴於單個原子值(int)來控制同步狀態,子類必須要定義獲取獲取同步與釋放狀態的方法,在AQS中提供了三種方法getState()setState(int newState)compareAndSetState(int expect, int update)來進行操作。同時子類應該為自定義同步元件的靜態內部類,AQS自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步元件使用,同步器既可以支援獨佔式地獲取同步狀態,也可以支援共享式地獲取同步狀態,這樣就可以方便實現不同型別的同步元件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

AQS類方法簡介

AQS的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器提供的模板方法,而這些模板方法將會呼叫使用者重寫的方法。

修改同步狀態方法

在子類實現自定義同步元件的時候,需要通過AQS提供的以下三個方法,來獲取與釋放同步狀態。

  • int getState() :獲取當前同步狀態
  • void setState(int newState) :設定當前同步狀態
  • boolean compareAndSetState(int expect, int update) 使用CAS設定當前狀態。

子類中可以重寫的方法

  • boolean isHeldExclusively():當前執行緒是否獨佔鎖
  • boolean tryAcquire(int arg):獨佔式嘗試獲取同步狀態,通過CAS操作設定同步狀態,如果成功返回true,反之返回false
  • boolean tryRelease(int arg):獨佔式釋放同步狀態。
  • int tryAcquireShared(int arg):共享式的獲取同步狀態,返回大於等於0的值,表示獲取成功,反之失敗。
  • boolean tryReleaseShared(int arg):共享式釋放同步狀態。

獲取同步狀態與釋放同步狀態方法

當我們實現自定義同步元件時,將會呼叫AQS對外提供的方法同步狀態與釋放的方法,當然這些方法內部會呼叫其子類的模板方法。這裡將對外提供的方法分為了兩類,具體如下所示:

  • 獨佔式獲取與釋放同步狀態
  1. void acquire(int arg):獨佔式獲取同步狀態,如果當前執行緒獲取同步狀態成功,則返回,否則進入同步佇列等待,該方法會呼叫tryAcquire(int arg)方法。
  2. void acquireInterruptibly(int arg):與 void acquire(int arg)基本邏輯相同,但是該方法響應中斷,如果當前沒有獲取到同步狀態,那麼就會進入等待佇列,如果當前執行緒被中斷(Thread().interrupt()),那麼該方法將會丟擲InterruptedException。並返回
  3. boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly(int arg)的基礎上,增加了超時限制,如果當前執行緒沒有獲取到同步狀態,那麼將返回fase,反之返回true。
  4. boolean release(int arg) :獨佔式的釋放同步狀態
  • 共享式獲取與釋放同步狀態
  1. void acquireShared(int arg):共享式的獲取同步狀態,如果當前執行緒未獲取到同步狀態,將會進入同步佇列等待,與獨佔式獲取的主要區別是在同一時刻可以有多個執行緒獲取到同步狀態。
  2. void acquireSharedInterruptibly(int arg):在acquireShared(int arg)的基本邏輯相同,增加了響應中斷。
  3. boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly的基礎上,增加了超時限制。
  4. boolean releaseShared(int arg) :共享式的釋放同步狀態

AQS具體實現及內部原理

在瞭解了AQS中的針對不同方式獲取與釋放同步狀態(獨佔式與共享式)與修改同步狀態的方法後,現在我們來了解AQS中具體的實現及其內部原理。

AQS中FIFO佇列

在上文中我們提到AQS中主要通過一個FIFO(first-in-first-out)來控制執行緒的同步。那麼在實際程式中,AQS會將獲取同步狀態的執行緒構造成一個Node節點,並將該節點加入到佇列中。如果該執行緒獲取同步狀態失敗會阻塞該執行緒,當同步狀態釋放時,會把頭節點中的執行緒喚醒,使其嘗試獲取同步狀態。

Node節點結構

下面我們就通過實際程式碼來了解Node節點中儲存的資訊。Node節點具體實現如下:

    static final class Node {
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
    }
複製程式碼

Node節點是AQS中的靜態內部類,下面分別對其中的屬性(注意其屬性都用volatile 關鍵字進行修飾)進行介紹。

  • int waitStatus:等待狀態主要包含以下狀態
  1. SIGNAL = -1:當前節點的執行緒如果釋放了或取消了同步狀態,將會將當前節點的狀態標誌位SINGAL,用於通知當前節點的下一節點,準備獲取同步狀態。
  2. CANCELLED = 1:被中斷或獲取同步狀態超時的執行緒將會被置為當前狀態,且該狀態下的執行緒不會再阻塞。
  3. CONDITION = -2:當前節點在Condition中的等待佇列上,(關於Condition會在下篇文章進行介紹),其他執行緒呼叫了Condition的singal()方法後,該節點會從等待佇列轉移到AQS的同步佇列中,等待獲取同步鎖。
  4. PROPAGATE = -3:與共享式獲取同步狀態有關,該狀態標識的節點對應執行緒處於可執行的狀態。
  5. 0:初始化狀態。
  • Node prev:當前節點在同步佇列中的上一個節點。
  • Node next:當前節點在同步佇列中的下一個節點。
  • Thread thread:當前轉換為Node節點的執行緒。
  • Node nextWaiter:當前節點在Condition中等待佇列上的下一個節點,(關於Condition會在下篇文章進行介紹)。

AQS同步佇列具體實現結構

通過上文的描述我們大概瞭解了Node節點中儲存的資料與資訊,現在我們來看看整個AQS下同步佇列的結構。具體如下圖所示:

aqs.png
在AQS中的同步佇列中,分別有兩個指標(你也可以叫做物件的引用),一個head指標指向佇列中的頭節點,一個tail指標指向佇列中的尾節點。

AQS新增尾節點

當一個執行緒成功獲取了同步狀態(或者鎖),其他執行緒無法獲取到同步狀態,這個時候會將該執行緒構造成Node節點,並加入到同步佇列中,而這個加入佇列的過程必須要確保執行緒安全,所以在AQS中提供了一個基於CAS的設定尾節點的方法:compareAndSetTail(Node expect,Nodeupdate),它需要傳遞當前執行緒“認為”的尾節點和當前節點,只有設定成功後,當前節點才正式與之前的尾節點建立關聯。具體過程如下圖所示:

aqs_save_tail.png
上圖中,虛線部分為之前tail指向的節點。

AQS新增頭節點

在AQS中的同步佇列中,頭節點是獲取同步狀態成功的節點,頭節點的執行緒會在釋放同步狀態時,將會喚醒其下一個節點,而下一個節點會在獲取同步狀態成功時將自己設定為頭節點,具體過程如下圖所示:

aqs_save_head.png

上圖中,虛線部分為之前head指向的節點。因為設定頭節點是獲取同步狀態成功的執行緒來完成的,由於只有一個執行緒能夠成功獲取到同步狀態,因此設定頭節點的方法並不需要CAS來進行保證,只需要將原頭節點的next指向斷開就行了。

現在我們已經瞭解了AQS中同步佇列的頭節點與尾節點的設定過程。現在我們根據實際程式碼進行分析,因為涉及到不同狀態對同步狀態的獲取(獨佔式與共享式),所以下面會分別對這兩種狀態進行講解。

獨佔式同步狀態獲取與釋放

獨佔式同步狀態獲取

通過acquire(int arg)方法我們可以獲取到同步狀態,但是需要注意的是該方法並不會響應執行緒的中斷與獲取同步狀態的超時機制。同時即使當前執行緒已經中斷了,通過該方法放入的同步佇列的Node節點(該執行緒構造的Node),也不會從同步佇列中移除。具體程式碼如下所示:

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

在該方法中,主要通過子類重寫的方法tryAcquire(arg)來獲取同步狀態,如果獲取同步狀態失敗,則會將請求執行緒構造獨佔式Node節點(Node.EXCLUSIVE),同時將該執行緒加入同步佇列的尾部(因為AQS中的佇列是FIFO型別)。接著我們檢視addWaiter(Node mode)方法具體細節:

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//將該執行緒構造成Node節點
  
        Node pred = tail;
        if (pred != null) {//嘗試將尾指標 tail 指向當前執行緒構造的Node節點
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
              //如果成功,那麼將尾指標之前指向的節點的next指向 當前執行緒構造的Node節點
                pred.next = node;
                return node;
            }
        }
        enq(node);//如果當前尾指標為null,則呼叫enq(final Node node)方法
        return node;
    }
複製程式碼

在該方法中,主要分為兩個步驟:

  • 如果當前尾指標(tail)不為null,那麼嘗試將尾指標 tail 指向當前執行緒構造的Node節點,如果成功,那麼將尾指標之前指向的節點的next指向當前執行緒構造的Node節點,並返回當前節點。
  • 反之呼叫enq(final Node node)方法,將當前執行緒構造的節點加入同步佇列中。

接下來我們繼續檢視enq(final Node node)方法。

  private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) {//如果當前尾指標為null,那麼嘗試將頭指標 head指向當前執行緒構造的Node節點
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {//如果當前尾指標(tail)不為null,那麼嘗試將尾指標 tail 指向當前執行緒構造的Node節點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

複製程式碼

在enq(final Node node)方法中,通過死迴圈(你也可以叫做自旋)的方式來保證節點的正確的新增。接下來,我們繼續檢視acquireQueued(final Node node, int arg)方法的處理。該方法才是整個多執行緒競爭同步狀態的關鍵,大家一定要注意看!!!

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//獲取該節點的上一節點
                //如果上一節點是head鎖指向的節點,且該節點獲取同步狀態成功
                if (p == head && tryAcquire(arg)) {
		            //設定head指向該節點,
                    setHead(node);
                    p.next = null; // 將上一節點的next指向斷開
                    failed = false;
                    return interrupted;
                }
                //判斷獲取同步狀態失敗的執行緒是否需要阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//阻塞並判斷當前執行緒是否已經中斷了
                    interrupted = true;
            }
        } finally {
            if (failed)
            //如果執行緒中斷了,那麼就將該執行緒從同步佇列中移除,同時喚醒下一節點
                cancelAcquire(node);
        }
    }
複製程式碼

在該方法中主要分為三個步驟:

  • 通過死迴圈(你也可以叫做自旋)的方式來獲取同步狀態,如果當前節點的上一節點是head指向的節點該節點獲取同步狀態成功,那麼會設定head指向該節點 ,同時將上一節點的next指向斷開。
    aqs_self_rotate.png

aqs_get_head.png

  • 如果當前節點的上一節點不是head指向的節點,或者獲取當前節點同步狀態失敗,那麼會先呼叫shouldParkAfterFailedAcquire(Node pred, Node node)方法來判斷是需要否阻塞當前執行緒,如果該方法返回true,則呼叫parkAndCheckInterrupt()方法來阻塞執行緒。如果該方法返回false,那麼該方法內部會把當前節點的上一節點的狀態修改為Node.SINGAL。
  • 在finally語句塊中,判斷當前執行緒是否已經中斷。如果中斷,則通過那麼cancelAcquire(Node node)方法將該執行緒(對應的Node節點)從同步佇列中移除,同時喚醒下一節點。

下面我們接著來看shouldParkAfterFailedAcquire(Node pred, Node node)方法,看看具體的阻塞具體邏輯,程式碼如下所示:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
	        //上一節點已經設定狀態請求釋放訊號,因此當前節點可以安全地阻塞
            return true;
        if (ws > 0) {
	        //上一節點,已經被中斷或者超時,那麼接跳過所有狀態為Node.CANCELLED
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
	        //其他狀態,則呼叫cas操作設定狀態為Node.SINGAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
複製程式碼

在該方法中會獲取上一節點的狀態(waitStatus),然後進行下面的三個步驟的判斷。

  • 如果上一節點狀態為Node.SIGNAL,那麼會阻塞接下來的執行緒(函式 return true)
  • 如果上一節點的狀態大於0(從上文描述的waitStatus所有狀態中,我們可以得知只有Node.CANCELLED大於0)那麼會跳過整個同步列表中所有狀態為Node.CANCELLED的Node節點。(函式 return false)
  • 如果上一節點是其他狀態,則呼叫CAS操作設定其狀態為Node.SINGAL。(函式 return false)
阻塞實現

shouldParkAfterFailedAcquire(Node pred, Node node)方法返回true時,接著會呼叫parkAndCheckInterrupt()方法來阻塞當前執行緒。該方法的返回值為當前執行緒是否中斷。

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

在該方法中,主要阻塞執行緒的方法是通過LockSupport(在後面的文章中會具體介紹)的park來阻塞當前執行緒。

從同步佇列中移除,同時喚醒下一節點

通過對獨佔式獲取同步狀態的理解,我們知道 acquireQueued(final Node node, int arg)方法中最終會執行finally語句塊中的程式碼,來判斷當前執行緒是否已經中斷。如果中斷,則通過那麼cancelAcquire(Node node)方法將該執行緒從同步佇列中移除。那麼接下來我們來看看該方法的具體實現。具體程式碼如下:

   private void cancelAcquire(Node node) {
        //如果當前節點已經不存在直接返回
        if (node == null)
            return;
		//(1)將該節點對應的執行緒置為null
        node.thread = null;

        //(2)跳過當前節點之前已經取消的節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

		//獲取在(2)操作之後,節點的下一個節點
        Node predNext = pred.next;

	    //(3)將當前中斷的執行緒對應節點狀態設定為CANCELLED
        node.waitStatus = Node.CANCELLED;

        //(4)如果當前中斷的節點是尾節點,那麼則將尾節點重新指向
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            //(5)如果中斷的節點的上一個節點的狀態,為SINGAL或者即將為SINGAL,
            //那麼將該當前中斷節點移除
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);//(6)將該節點移除,同時喚醒下一個節點
            }

            node.next = node; // help GC
        }
    }
複製程式碼

觀察上訴程式碼,我們可以知道該方法幹了以下這幾件事

  • (1)將中斷執行緒對應的節點對應的執行緒置為null

  • (2)跳過當前節點之前已經取消的節點(我們已經知道在Node.waitStatus的列舉中,只有CANCELLED 大於0 )

    跳過已經取消的節點.png

  • (3)將當前中斷的執行緒對應節點狀態設定為CANCELLED

  • (4)在(2)的前提下,如果當前中斷的節點是尾節點,那麼通過CAS操作將尾節點指向(2)操作後的的節點。

重新設定尾節點Tail.png

  • (5)如果當前中斷節點不是尾節點,且當前中斷的節點的上一個節點的狀態,為SINGAL或者即將為SINGAL,那麼將該當前中斷節點移除。
  • (6)如果(5)條件不滿足,那麼呼叫unparkSuccessor(Node node)方法將該節點移除,同時喚醒下一個節點。具體程式碼如下:
    private void unparkSuccessor(Node node) {
         //重置該節點為初始狀態
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //獲取中斷節點的下一節點    
        Node s = node.next;
        //判斷下一節點的狀態,如果為Node.CANCELED狀態
        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,則喚醒該節點中的執行緒。
        if (s != null)
            LockSupport.unpark(s.thread);
    }
複製程式碼

這裡為了方便大家理解,我還是將圖補充了出來,(圖片有可能不是很清晰,建議大家點選瀏覽大圖),

aqs.png
整體來說,unparkSuccessor(Node node)方法主要是獲取中斷節點後的可用節點(Node.waitStatus<=0),然後將該節點對應的執行緒喚醒。

獨佔式同步狀態釋放

當執行緒獲取同步狀態成功並執行相應邏輯後,需要釋放同步狀態,使得後繼執行緒節點能夠繼續獲取同步狀態,通過呼叫AQS的relase(int arg)方法,可以釋放同步狀態。具體程式碼如下:

 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(int arg),也就是說同步狀態的釋放邏輯,是需要使用者來自己定義的。當tryRelease(int arg)方法返回true後,如果當前頭節點不為null且頭節點waitStatus!=0,接著會呼叫unparkSuccessor(Node node)方法來喚醒下一節點(使其嘗試獲取同步狀態)。關於unparkSuccessor(Node node)方法,上文已經分析過了,這裡就不再進行描述了。

共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻是否能有多個執行緒同時獲取到同步狀態。以檔案的讀寫為例,如果一個程式在對檔案進行讀操作,那麼這一時刻對於檔案的寫操作均會被阻塞。而其他讀操作能夠同時進行。如果對檔案進行寫操作,那麼這一時刻其他的讀寫操作都會被阻塞,寫操作要求對資源的獨佔式訪問,而讀操作可以是共享訪問的。

共享式同步狀態獲取

在瞭解了共享式同步狀態獲取與獨佔式獲取同步狀態的區別後,現在我們來看一看共享式獲取的相關方法。在AQS中通過 acquireShared(int arg)方法來實現的。具體程式碼如下:

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

在該方法內部會呼叫模板方法tryAcquireShared(int arg),同獨佔式獲取獲取同步同步狀態一樣,也是需要使用者自定義的。當tryAcquireShared(int arg)方法返回值小於0時,表示沒有獲取到同步狀態,則呼叫doAcquireShared(int arg)方法獲取同步狀態。反之,已經獲取同步狀態成功,則不進行任何的操作。關於doAcquireShared(int arg)方法具體實現如下所示:

   private void doAcquireShared(int arg) {
	    //(1)新增共享式節點在AQS中FIFO佇列中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //(2)自旋獲取同步狀態
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
	                    //當獲取同步狀態成功後,設定head指標
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //(3)判斷執行緒是否需要阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
	        //(4)如果執行緒已經中斷,則喚醒下一節點
            if (failed)
                cancelAcquire(node);
        }
    }
複製程式碼

整體來看,共享式獲取的邏輯與獨佔式獲取的邏輯幾乎一樣,還是以下幾個步驟:

  • (1)新增共享式節點在AQS中FIFO佇列中,這裡需要注意節點的構造為 addWaiter(Node.SHARED),其中 Node.SHARED為Node類中的靜態常量(static final Node SHARED = new Node()),且通過addWaiter(Node.SHARED)方法構造的節點狀態為初始狀態,也就是waitStatus= 0

  • (2)自旋獲取同步狀態,如果當前節點的上一節點為head節點,其獲取同步狀態成功,那麼將呼叫setHeadAndPropagate(node, r);,重新設定head指向當前節點。同時重新設定該節點狀態waitStutas = Node.PROPAGATE(共享狀態),然後直接退出doAcquireShared(int arg)方法。具體情況如下圖所示:

共享式自旋判斷.png

  • (3)如果不滿足條件(2),那麼會判斷當前節點的上一節點不是head指向的節點,或者獲取當前節點同步狀態失敗,那麼會先呼叫shouldParkAfterFailedAcquire(Node pred, Node node)方法來判斷是需要否阻塞當前執行緒,如果該方法返回true,則呼叫parkAndCheckInterrupt()方法來阻塞執行緒。如果該方法返回false,那麼該方法內部會把當前節點的上一節點的狀態修改為Node.SINGAL。具體情況如下圖所示:

共享式執行緒阻塞.png

  • (4)如果執行緒已經中斷,則喚醒下一節點

前面我們提到了,共享式與獨佔式獲取同步狀態的主要不同在於其設定head指標的方式不同,下面我們就來看看共享式設定head指標的方法setHeadAndPropagate(Node node, int propagate)。具體程式碼如下:

    private void setHeadAndPropagate(Node node, int propagate) {
	    //(1)設定head 指標,指向該節點
        Node h = head; // Record old head for check below
        setHead(node);
        
        //(2)判斷是否執行doReleaseShared();
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果當前節點的下一節點是共享式獲取同步狀態節點,則呼叫doReleaseShared()方法
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
複製程式碼

在setHeadAndPropagate(Node node, int propagate)方法中有兩個引數。 第一個引數node是當前共享式獲取同步狀態的執行緒節點。 第二個引數propagate(中文意思,繁殖、傳播)是共享式獲取同步狀態執行緒節點的個數。

其主要邏輯步驟分為以下兩個步驟:

  • (1)設定head 指標,指向該節點。從中我們可以看出在共享式獲取中,Head節點總是指向最進獲取成功的執行緒節點!!!
  • (2)判斷是否執行doReleaseShared(),從程式碼中我們可以得出,主要通過該條件if (s == null || s.isShared()),其中 s為當前節點的下一節點(也就是說同一時刻有可能會有多個執行緒同時訪問)。當該條件為true時,會呼叫doReleaseShared()方法。關於怎麼判斷下一節點是否是否共享式執行緒節點,具體邏輯如下:
   //在共享式訪問中,當前節點為SHARED型別
   final Node node = addWaiter(Node.SHARED);
   
   //在呼叫addWaiter 內部會呼叫Node構造方法,其中會將nextWaiter設定為Node.SHARED。
   Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
   //SHARED為Node類靜態類    
   final boolean isShared() {
            return nextWaiter == SHARED;
        }
        
複製程式碼

下面我們繼續檢視doReleaseShared()方法的具體實現,具體程式碼如下所示:

 private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
	                //(1)從上圖中,我們可以得知在共享式的同步佇列中,如果存在堵塞節點,
	                //那麼head所指向的節點狀態肯定為Node.SINGAL,
	                //通過CAS操作將head所指向的節點狀態設定為初始狀態,如果成功就喚醒head下一個阻塞的執行緒
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);//喚醒下一節點執行緒,上文分析過該方法,這裡就不在講了
                }
				//(2)表示該節點執行緒已經獲取共享狀態成功,則通過CAS操作將該執行緒節點狀態設定為Node.PROPAGATE
				//從上圖中,我們可以得知在共享式的同步佇列中,
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   //如果head指標發生改變一直迴圈,否則跳出迴圈
                break;
        }
    }
複製程式碼

從程式碼中我們可以看出該方法主要分為兩個步驟:

  • (1)從上圖中,我們可以得知在共享式的同步佇列中,如果存在堵塞節點,那麼head所指向的節點狀態肯定為Node.SINGAL,通過CAS操作將head所指向的節點狀態設定為初始狀態,如果成功就喚醒head下一個阻塞的執行緒節點,反之繼續迴圈。
  • (2)如果(1)條件不滿足,那麼說明該節點已經獲取成功的獲取同步狀態,那麼通過CAS操作將該執行緒節點的狀態設定為waitStatus = Node.PROPAGATE,如果CAS操作失敗,就一直迴圈。
共享式同步狀態釋放

當執行緒獲取同步狀態成功並執行相應邏輯後,需要釋放同步狀態,使得後繼執行緒節點能夠繼續獲取同步狀態,通過呼叫AQS的releaseShared(int arg)方法,可以釋放同步狀態。具體程式碼如下:

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

獨佔式與共享式超時獲取同步狀態

因為獨佔式與共享式超時獲取同步狀態,與其本身的非超時獲取同步狀態邏輯幾乎一樣。所以下面就以獨佔式超時獲取同步狀態的相應邏輯進行講解。

在獨佔式超時獲取同步狀態中,會呼叫tryAcquireNanos(int arg, long nanosTimeout)方法,其中具體nanosTimeout引數為你傳入的超時時間(單位納秒),具體程式碼如下所示:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
複製程式碼

觀察程式碼,我們可以得知如果當前執行緒已經中斷,會直接丟擲InterruptedException,如果當前執行緒能夠獲取同步狀態( 呼叫tryAcquire(arg)),那麼就會直接返回,如果當前執行緒獲取同步狀態失敗,則呼叫doAcquireNanos(int arg, long nanosTimeout)方法來超時獲取同步狀態。那下面我們接著來看該方法具體程式碼實現,程式碼如下圖所示:

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;
                }
                //如果獲取同步狀態失敗,計算的剩下的時間
                nanosTimeout = deadline - System.nanoTime();
                //(3)如果超時直接退出
                if (nanosTimeout <= 0L)
                    return false;
                //(4)如果沒有超時,且nanosTimeout大於spinForTimeoutThreshold(1000納秒)時,
                //則讓執行緒等待nanosTimeout (剩下的時間,單位:納秒。)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                //(5)如果當前執行緒被中斷,直接丟擲異常    
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製程式碼

整個方法為以下幾個步驟:

  • (1)線上程獲取同步狀態之前,先計算出超時等待的結束時間。(單位精確到納秒)
  • (2)通過自旋操作獲取同步狀態,如果成功,則直接返回
  • (3)如果獲取同步失敗,則計算剩下的時間。如果已經超時了就直接退出。
  • (4)如果沒有超時,則判斷當前剩餘時間nanosTimeout是否大於spinForTimeoutThreshold(1000納秒),如果大於,則通過 LockSupport.parkNanos(this, nanosTimeout)方法讓執行緒等待相應時間。(該方法會在根據傳入的nanosTimeout時間,等待相應時間後返回。),如果nanosTimeout小於等於spinForTimeoutThreshold時,將不會使該執行緒進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,執行緒會進入無條件的快速自旋。
  • (5)在沒有走(4)步驟的情況下,表示當前執行緒已經被中斷了,則直接丟擲InterruptedException

最後

到現在我們基本瞭解了整個AQS的內部結構與其獨佔式與共享式獲取同步狀態的實現,但是其中涉及到的執行緒的阻塞、等待、喚醒(與LockSupport工具類相關)相關知識點我們都沒有具體介紹,後續的文章會對LockSupport工具以及後期關於鎖相關的等待/通知模式相關的Condition介面進行介紹。希望大家繼續保持著學習的動力~~。

總結

  • 整個AQS是基於其內部的FIFO佇列實現同步控制。請求的執行緒會封裝為Node節點。
  • AQS分為整體分為獨佔式與共享式獲取同步狀態。其支援執行緒的中斷,與超時獲取。

相關文章