CountDownLatch是AbstractQueuedSynchronizer中共享鎖模式的一個的實現,是一個同步工具類,用來協調多個執行緒之間的同步。CountDownLatch能夠使一個或多個執行緒在等待另外一些執行緒完成各自工作之後,再繼續執行。CountDownLatch內部使用一個計數器進行實現執行緒通知條件,計數器初始值為進行通知執行緒的數量。當每一個通知執行緒完成自己任務後,計數器的值就會減一。當計數器的值為0時,表示所有的通知執行緒都已經完成一些任務,然後在CountDownLatch上所有等待的執行緒就可以恢復執行接下來的任務。基本上CountDownLatch的原理就是這樣,下面我們一起去看看原始碼。
public class CountDownLatch { private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } .... } private final Sync sync; public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } .... }
從上面簡略的原始碼可以看出,CountDownLatch和ReentrantLock一樣,在內部宣告瞭一個繼承AbstractQueuedSynchronizer的Sync內部類(重寫了父類的tryAcquireShared(int acquires)和tryReleaseShared(int releases)),並在宣告瞭一個sync屬性。CountDownLatch只有一個有參構造器,引數count就是上面說的的進行通知的執行緒數目,說白點也就是countDown()方法需要被呼叫的次數。
CountDownLatch的主要方法是wait()和countDown(),我們先看wait()方法原始碼。
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
和ReentrantLock一樣,CountDownLatch依然算是一件外衣,實際還是靠sync進行操作。我們接著看AQS的acquireSharedInterruptibly(int arg)方法(實際上引數在CountDownLatch裡沒什麼用)
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
看到先判斷當前執行緒是否是中斷狀態,然後呼叫子類重寫的tryAcquireShared(int acquires)方法去判斷是否需要進行阻塞(也即是嘗試獲取鎖),如果返回值小於0 ,就呼叫doAcquireSharedInterruptibly(int acquires)方法進行執行緒阻塞。先看tryAcquireShared(int acquires)方法
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
原始碼很簡單,就是看下state是否等於0,等於0,就返回1,代表不需要執行緒阻塞,不等於0(實際上state只會大於或者等於0),就返回-1,表示需要進行執行緒阻塞。這裡有個伏筆就是如果CountDownLatch的計數器state被減至0時,後續再有執行緒呼叫CountDownLatch的wait()方法時,會直接往下執行呼叫者方法的程式碼,不會造成執行緒阻塞。
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
在doAcquireSharedInterruptibly(int acquires)方法中進行執行緒阻塞的步驟依然是先呼叫addWaiter(Node mode)方法將該執行緒封裝到AQS內部的CLH佇列的Node.SHARE(共享)模式的Node節點,並放入到隊尾,然後在迴圈中去嘗試持有鎖和進行執行緒阻塞。在迴圈體內,先獲取到前任隊尾,然後判斷前任隊尾是否是隊首head,如果是就呼叫tryAcquireShared(int acquires)嘗試獲取鎖,如果返回1表示獲取到了鎖,就呼叫setHeadAndPropagate(Node node, int propagate)方法將節點設定head然後再往下傳播,解除後續節點的執行緒阻塞狀態(這就是共享鎖的核心)。如果返回-1,表示沒有獲取到鎖,就呼叫shouldParkAfterFailedAcquire(Node pre, Node node)進行pre節點的waitStatus賦值為Node.SIGNAL,然後在墨跡一次迴圈,呼叫parkAndCheckInterrupt()方法進行執行緒阻塞。我們先看setHeadAndPropagate(Node node, int propagate)方法原始碼
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) // 釋放共享鎖,這是這一步是最關鍵的一步 doReleaseShared(); } }
在setHeadAndPropagate(Node node, int propagate)方法中,直接將node設定從head,因為引數propagate為始終為1(到這一步就表示獲取了共享鎖,state等於0,在tryAcquireShared(int acquires)方法中就只會返回1),所以也就是後面直接獲取head的next節點,如果head的next節點存在,並且是共享模式,就呼叫doReleaseShared()方法去釋放CLH中head的next節點。
shouldParkAfterFailedAcquire(Node pre, Node node)和parkAndCheckInterrupt()兩個方法在JUC之ReentrantLock原始碼分析一篇部落格中已經講過了,這裡不再贅述了。
doReleaseShared()這個方法其實也是countDown()方法的核心實現,這裡先賣個關子,後面會講到。我們接著看doReleaseShared()方法。
private void doReleaseShared() { 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; } }
當呼叫wait()方法進行執行緒阻塞等待被別的執行緒解除阻塞後,對於AQS中共享鎖最核心的程式碼就是doReleaseShared()這個方法。先獲取head節點,如果head節點存在並且有後續節點,就先判斷head節點的狀態,如果狀態是Node.SIGNAL(表示後續節點需要鎖釋放通知),將head節點狀態改為0,然後解除下一節點的執行緒阻塞狀態,然後判斷下之前獲取的head和現在的head是否還是同一個,如果是就結束迴圈,如果不是,那就接著迴圈。其實就算是存線上程在執行完unparkSuccessor(Node node)方法後失去了CPU的執行權,一直到被解除執行緒阻塞的next節點坐上了head節點後才有機會獲取到CPU執行權這種情況,無非就是之前獲取到head和現在的head不相同了,大不了再迴圈一次,也算是多執行緒去解除當前head節點的next節點執行緒阻塞,影響不大;如果狀態是0,嘗試將狀態有0改為Node.PROPAGATE,然後重複迴圈,head節點是0的這種狀態在CountDownLatch中應該是不會出現的,可能是AQS對別的類的相容,也可能是我眼拙沒看出來。如果head為null或者head與tail相同,就結束迴圈。
到這裡CountDownLatch的wait()方法就分析完成了,這裡總結下wait()方法流程:
1、如果state是0,直接往下執行呼叫者的程式碼,不會進行執行緒等待阻塞
2、將當前執行緒封裝到共享模式的Node節點中,然後放入到CLH佇列的隊尾
3、將前任隊尾的waitStatus改變為Node.SIGNAL,然後呼叫LockSupport.park()阻塞當前執行緒,等待前節點喚醒
4、被前節點喚醒後,把自己設為head,然後去喚醒next節點
我們看完了wait()方法,現在在來看下countDown()方法的原始碼
public void countDown() { sync.releaseShared(1); }
一如既往的簡單,直接呼叫AQS的releaseShared(int arg)方法,我們直接去看AQS的releaseShared(int arg)方法
AQS方法 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } CountDownLatch方法 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } }
在AQS的releaseShared(int arg)中先去呼叫一定要被子類重寫的tryReleaseShared(int releases)方法,返回值表示是否需要進行喚醒操作,如果返回true,那就呼叫doReleaseShared()方法去解除head節點的next節點執行緒阻塞狀態。是的,你沒看錯,就是我們前面分析的doReleaseShared()方法,他們複用了同一個方法。而在CountDownLatch的tryReleaseShared(int releases)方法實現也非常簡單,就是判斷下當前state是否是0,如果是0,表示已經減完了,不需要再減了,等待執行緒已經在被依次喚醒了,返回false表示不需要去喚醒後續節點了。最後再看看減完後的state是否是等於0,等於0,表示需要去解除後續節點的阻塞狀態;不等於0(其實是大於0),表示呼叫countDown()方法去減state的次數還不夠,暫時還不能解除後續節點的阻塞狀態。
countDown()方法比較簡單,我們也總結下countDown()流程:
1、如果state為0,表示已經有執行緒在解除CLH佇列節點的阻塞狀態了,這裡直接結束
2、如果state減去1後不等於0,表示還要等有執行緒再次呼叫countDown()方法進行state減1,這裡直接結束
3、如果state減去1後等於0,表示已經執行緒呼叫countDown()方法的次數已經達到最初設定的次數,然後去解除CLH佇列上節點的阻塞狀態