AQS(AbstractQueuedSynchronizer)隊列同步器源碼閱讀(一)

在看這篇文章之前可以根據我上一篇文章來對隊列同步器AQS的應用和意義有一個基礎印象。
http://www.lxweimin.com/p/e2f339d654f7

從實現角度分析同步器是如何完成線程同步的,主要包括:同步隊列、獨占式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心數據結構與模板方法。

1.同步隊列

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成為一個節點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和后繼節點,節點的屬性類型與名稱以及描述如表:

節點是構成同步隊列的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的線程將會成為節點加入該隊列的尾部,同步隊列的基本結構如圖:

同步器包含了兩個節點類型的引用,一個指向頭節點,而另一個指向尾節點。試想一下,當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造成為節點并加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基于CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認為”的尾節點和當前節點,只有設置成功后,當前節點才正式與之前的尾節點建立關聯。
static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

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

       volatile int waitStatus //節點狀態
       volatile Node prev //當前節點/線程的前驅節點
       volatile Node next; //當前節點/線程的后繼節點
       volatile Thread thread;//加入同步隊列的線程引用
       Node nextWaiter;//等待隊列中的下一個節點

        final boolean isShared() {
            return nextWaiter == SHARED;
        }
      
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    
        }

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

同步器將節點加入到同步隊列的過程如圖:


節點加入到同步隊列

節點加入到同步隊列同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒后繼節點,而后繼節點將會在獲取同步狀態成功時將自己設置為首節點,該過程如圖:
首節點的設置

設置首節點是通過獲取同步狀態成功的線程來完成的,由于只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法并不需要使用CAS來保證,它只需要將首節點設置成為原首節點的后繼節點并斷開原首節點的next引用即可。

2.獨占式同步狀態獲取與釋放

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由于線程獲取同步狀態失敗后進入同步隊列中,后續對線程進行中斷操作時,線程不會從同步隊列中移出,該方法代碼:

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

//tryAcquire(arg)   嘗試以獨占模式獲取。 該方法應該查詢對象的狀態是否允許以獨占模式獲取,如果是,則獲取它。
//該方法總是由執行獲取的線程調用。 如果此方法報告失敗,則獲取方法可能將線程排隊(如果尚未排隊),
//直到被其他線程釋放為止。

 /** 
  *  addWaiter(Node.EXCLUSIVE)  為當前線程和給定模式創建和排隊節點。
  *  selfInterrupt    Thread.currentThread().interrupt();
  */

主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨占式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)并通過addWaiter(Node node)。
方法將該節點加入到同步隊列的尾部,最后調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

分析一下相關工作。首先是節點的構造以及加入同步隊列:

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

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

通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全添加.compareAndSetTail該方法由unsafe類提供,有關于想要深入了解UnSafe類得,可以看以下得文章:
https://www.cnblogs.com/throwable/p/9139947.html
如果使用一個普通的LinkedList來維護節點之間的關系,那么當一個線程獲取了同步狀態,而其他多個線程由于調用tryAcquire(int arg)方法獲取同步狀態失敗而并發地被添加到LinkedList時,LinkedList將難以保證Node的正確添加,最終的結果可能是節點的數量有偏差,而且順序也是混亂的。

在enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成為尾節點之后,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。可以看出,enq(final Node node)方法將并發添加節點的請求通過CAS變得“串行化”了。

節點進入同步隊列之后,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節點的線程)

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


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            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();
}

acquireQueued()在自旋過程中主要完成了兩件事情:

如果當前節點的前驅節點是頭節點,并且能夠獲得同步狀態的話,當前線程能夠獲得鎖該方法執行結束退出;
獲取鎖失敗的話,先將節點狀態設置成SIGNAL,然后調用LookSupport.park方法使得當前線程阻塞。

在acquireQueued(final Node node,int arg)方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是為什么?原因有兩個,如下。
第一,頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之后,將會喚醒其后繼節點,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點。
第二,維護同步隊列的FIFO原則。該方法中,節點自旋獲取同步狀態的行為如圖:


獨占式同步狀態獲取流程,也就是acquire(int arg)方法調用流程
獨占式同步狀態獲取流程

前驅節點為頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲
取同步狀態的自旋過程。當同步狀態獲取成功之后,當前線程從acquire(int arg)方法返回,如果對于鎖這種并發組件而言,代表著當前線程獲取了鎖。

當前線程獲取同步狀態并執行了相應邏輯之后,就需要釋放同步狀態,使得后續節點能夠繼續獲取同步狀態。通過調用同步器的release(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;
    }

該方法執行時,會喚醒頭節點的后繼節點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處于等待狀態的線程。

private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //頭節點的后繼節點
    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);
}

源碼的關鍵信息請看注釋,首先獲取頭節點的后繼節點,當后繼節點為null的時候會調用LookSupport.unpark()方法,該方法會喚醒該節點的后繼節點所包裝的線程。因此,每一次鎖釋放后就會喚醒隊列中該節點的后繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。

線程獲取鎖失敗,線程被封裝成Node進行入隊操作,核心方法在于addWaiter()和enq(),同時enq()完成對同步隊列的頭結點初始化工作以及CAS操作失敗的重試;

線程獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點并且成功獲得同步狀態時,節點出隊即該節點引用的線程獲得鎖,否則,當不滿足條件時就會調用LookSupport.park()方法使得線程阻塞;
釋放鎖的時候會喚醒后繼節點;

總結:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。

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

可響應中斷式鎖可調用方法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時即線程阻塞時該線程被中斷,代碼拋出被中斷異常。

超時等待式獲取同步狀態(tryAcquireNanos()方法)

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

在超時時間內,當前線程成功獲取了鎖;
當前線程在超時時間內被中斷;
超時時間結束,仍未獲得鎖返回false。

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

通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。

在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標志位會被修改,但線程依舊會阻塞在synchronized上,等待著獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回并拋出InterruptedException。

超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,
doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大于0則表示超時時間未到,需要繼續睡眠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();
                //如果獲取到鎖,則把頭節點出隊列,并返回。
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //獲取不到同步狀態,則計算當前時間,判斷是否超時,超時返回false
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
              // 3.線程阻塞等待
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
              //  如果被中斷,拋異常。
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

該方法在自旋過程中,當節點的前驅節點為頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨占式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小于等于0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然后使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object blocker,long nanos)方法返回)。這個后續看LockSupport的源碼。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在于,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。


待續二,休息一下

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容