【JUC】CountDownLatch源碼分析

1.什麼是CountDownLatch

  • 之前寫了一篇關於JUC包裏關於併發編程工具類的筆記,但過後總感覺不太舒服,不通透,所以,這次就想着先把整個CountDownLatch源碼擼一遍。

  • 至於CountDownLatch是什麼,擼代碼前,可先看:【JUC】CountDownLatch/CyclicBarrier/Semaphore

2.開擼

2.1 類圖
先從類圖開始,一步一步看。

  • idea找JUC包裏面的CountDownLatch,右鍵如圖:
    在這裏插入圖片描述
  • 打開類圖:
    在這裏插入圖片描述
  • 能看到CountDownLatch有一個Sync抽象類,找到Sync我們右鍵打開它的類圖:
    在這裏插入圖片描述
  • Sync的類圖:
    在這裏插入圖片描述
  • Sync繼承於AbstractQueuedSynchronizer
  • 而AbstractQueuedSynchronizer裏面維護了一個以Node爲節點的AQS隊列。
  • AQS隊列,就是CountDownLatch的核心。

2.2 AQS隊列

  • 追根溯源,AQS隊列是由AbstractQueuedSynchronizer維護
  • 而AbstractQueuedSynchronizer由繼承於AbstractOwnableSynchronizer
  • 我們先看AbstractOwnableSynchronizer源碼:
public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    protected AbstractOwnableSynchronizer() { }
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

  • exclusiveOwnerThread從名字就能看出來獨家擁有線程,也就是獨佔模式鎖的擁有者。
  • 而AQS定義兩種資源共享方式:

1,Exclusive(獨佔,只有一個線程能執行,ReentrantLock使用該模式)

2,Share(共享,多個線程可同時執行,Semaphore/CountDownLatch使用該模式)。

  • AQS隊列底層其實就是一個雙向鏈表,而在AbstractQueuedSynchronizer中鏈表節點是有內部類Node來充當,在Node裏有這麼兩行代碼:
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;


  • exclusiveMarker to indicate a node is waiting in shared mode
    共享模式:CountDownLatch使用該模式,今天我們聊的就是這個。
  • Marker to indicate a node is waiting in exclusive mode
    獨佔模式:ReentrantLock使用該模式,詳細可見:【鎖】【JUC】可重入鎖/AQS隊列–ReentrantLock源碼分析
  • 再看一下AbstractQueuedSynchronizer的核心成員:
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;
  
    protected AbstractQueuedSynchronizer() { }

   //隊列節點
    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;
        
        /** waitStatus value to indicate thread has cancelled */ 線程已經被取消,該狀態的節點不會再次阻塞。
		static final int CANCELLED =  1;
		/** waitStatus value to indicate successor's thread needs unparking */ 線程需要去被喚醒
		static final int SIGNAL    = -1;
		/** waitStatus value to indicate thread is waiting on condition */ 線程正在喚醒等待條件
		static final int CONDITION = -2;
		/**
		 * waitStatus value to indicate the next acquireShared should //線程的共享鎖應該被無條件傳播
		 * unconditionally propagate
		 */
		static final int PROPAGATE = -3;
        
        //上面四個值就是下面變量waitStatus的值
        volatile int waitStatus;
        //前一個節點
        volatile Node prev;
        //下一個節點
        volatile Node next;
        //當前節點代表的線程
        volatile Thread thread;
        /**
        *等待節點的後繼節點。如果當前節點是共享的,那麼這個字段是一個SHARED常量,也就是說節點類型(獨佔和共享)和
        *等待隊列中的後繼節點共用一個字段。(注:比如說當前節點A是共享的,那麼它的這個字段是shared,也就是說在這個等
        *待隊列中,A節點的後繼節點也是shared。如果A節點不是共享的,那麼它的nextWaiter就不是一個SHARED常量,即是獨
        *佔的。
        */
        Node nextWaiter;
}
    //頭結點
    private transient volatile Node head;
     //尾節點
    private transient volatile Node tail;
    //狀態值
    private volatile int state;
  • 如果瞭解CLH隊列的話你會感覺眼熟,因爲AQS隊列就是它的一個變體,至於CLH隊列是什麼,可見:【鎖】自旋鎖-MCS/CLH隊列
  • AbstractQueuedSynchronizer的這些成員中,volatile修飾的state是重點,volatile關鍵字保證了線程間可見性。可見:【JUC】volatile關鍵字相關整理
  • CountDownLatch中,state的值代表着待達到條件的線程數,比如初始化爲五,表示待達到條件的線程數爲5,每次調用countDown()函數都會減一,等到state變爲0,阻塞在隊列裏的線程就可以被重新喚醒了,下面會詳細聊。
  • state是被volatile修飾的,在多線程併發時,採用CAS修改state的值,至於什麼是CAS,可見:【JUC】 Java中的CAS
  • 另一個要注意的AbstractQueuedSynchronizer裏Node中的狀態屬性waitStatus,它默認爲零,還有四個值見上面Node代碼,它的值會被後置結點在獲取鎖失敗後阻塞前修改,用於提醒你在釋放鎖後去喚醒它,具體詳細情況後面源碼聊。
  • AQS結構圖
    在這裏插入圖片描述
  • AQS隊列的頭結點並不關聯任何線程,他是一個默認的Node節點。

2.3 開始擼代碼

  • 先看構造函數:
  public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        //初始化AQS隊列
        this.sync = new Sync(count);
    }


//內部類,AQS隊列
private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        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;
            }
        }
    }
  • CountDownLatch初始化時會初始化內部類Sync中的state的值,代表線程數。
  • CountDownLatch中的核心方法一個是await()方法,另一個是countDown()方法。

1,調用await()方法的線程會被掛起,它會等待直到state值爲0才繼續執行

2,調用countDown()將state值減1

2.3.1 我們先看await()方法

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
  • await()調用了acquireSharedInterruptibly()方法,該方法在sync的父類AbstractQueuedSynchronizer中:
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
            //判斷是否被中斷,中斷就拋出異常
        if (Thread.interrupted())
            throw new InterruptedException();
            //與tryAcquireShared(arg)的返回值相比較
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
  • 首先判斷是否被中斷,中斷就拋出異常
  • 否則的話與tryAcquireShared(arg)的返回值相比較:
 protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
  • tryAcquireShared(arg)中state不等於0將會返回-1,進入doAcquireSharedInterruptibly(arg)方法。
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //將當前線程相關的節點將入鏈表尾部
        final Node node = addWaiter(Node.SHARED);
        ////是否中斷
        boolean failed = true;
        try {
            for (;;) {
                // //獲取前一個節點
                final Node p = node.predecessor();
                //如果當前node節點是第二個節點,緊跟在head後面
                if (p == head) {
                  //判斷state是否等於0,等於0,返回1
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                       //重新設置頭結點,並喚醒後置結點
                        setHeadAndPropagate(node, r);
                        p.next = null; // 輔助 GC
                        failed = false;
                        return;
                    }
                }
             //當shouldParkAfterFailedAcquire返回成功,
            //也就是前驅節點是Node.SIGNAL狀態時,
            //進行真正的park將當前線程掛起,並且檢查中斷標記,
            //如果是已經中斷,則設置interrupted =true。
            //如果shouldParkAfterFailedAcquire返回false,
            //則重複上述過程,直到獲取到資源或者被park。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 我代碼中註釋寫的已經比較細了,可以大致看出整個邏輯:

1,調用await()方法的線程會把自己塞進一個node結點中。

2,該結點會把自己塞到AQS隊列最後面。

3,然後會進入一段類似自旋的代碼。

4,這段代碼中,會先判斷自己是不是在頭結點正後面,state值有沒有等於0,不是的話就修改前結點的waitStatus值爲-1,然後阻塞當前線程,等待前置結點的喚醒

5,等到被喚醒了,線程繼續自旋,判斷自己是不是已經到了頭結點後面,state值有沒有等於0,如果等於0了,就重置頭結點,然後喚醒下一個線程。

6,至於第一個被喚醒的線程,是在其他線程調用countDown()方法,直到state值等於0,後續會細講。

  • 接下來我們對doAcquireSharedInterruptibly(int arg)方法中調用的方法一個一個看;

addWaiter(Node.SHARED)

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //1、首先嚐試以快速方式添加到隊列末尾
    Node pred = tail;//pred指向現有tail末尾節點
    if (pred != null) {
    //新加入節點的前一個節點是現有AQS隊列的tail節點
        node.prev = pred;
      	//CAS原子性的修改tail節點
        if (compareAndSetTail(pred, node)) {    
            //修改成功,新節點成功加入AQS隊列,pred節點的next節點指向新的節點
            pred.next = node;
            return node;
        }
    }
    //2、pred爲空,或者修改tail節點失敗,
    //則走enq方法將節點插入隊列
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for(;;) {//CAS
        Node t = tail;
        if (t == null) { 
        // 必須初始化。這裏是AQS隊列爲空的情況。
        //通過CAS的方式創建head節點,並且tail和head都指向
        //同一個節點。
            if (compareAndSetHead(new Node()))
            //注意這裏初始化head節點,並不關聯任何線程!!
                tail = head;
        } else {
        //這裏變更node節點的prev指針,並且移動tail指針指向node,
        //前一個節點的next指向新插入的node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • 上面addWaiter方法的第一行代碼new Node(Thread.currentThread(), mode);,會創建一個node對象,Node.SHARED,上面我們聊過,SHARED:共享模式。
  • addWaiter首先會以快速方式將node添加到隊尾,如果失敗則走enq方法。失敗有兩種可能,一個是tail爲空,也就是AQS爲空的情況下。另一是compareAndSetTail失敗,也就是多線程併發添加到隊尾,此時會出現CAS失敗。
  • 注意enq方法,在t==null時,首先創建空的頭節點,不關聯任何的線程,nextWaiter和thread變量都是null。

shouldParkAfterFailedAcquire(Node pred, Node node)和parkAndCheckInterrupt()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws= pred.waitStatus;//獲取到上一個節點的waitStatus
    if (ws == Node.SIGNAL)//前面講到當一個節點狀態時SIGNAL時,
    //他有責任喚醒後面的節點。所以這裏判斷前驅節點是SIGNAL狀態,
    //則可以安心的park中斷了。
        return true;
    if (ws > 0) {
        /*
         * 過濾掉中間cancel狀態的節點
         * 前驅節點被取消的情況(線程允許被取消哦)。向前遍歷,
         * 直到找到一個waitStatus大於0的(不是取消狀態或初始狀態)
         * 的節點,該節點設置爲當前node的前驅節點。
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 修改前驅節點的WaitStatus爲Node.SIGNAL。
         * 明確前驅節點必須爲Node.SIGNAL,當前節點纔可以park 
         * 注意,這個CAS也可能會失敗,因爲前驅節點的WaitStatus狀態
         * 可能會發生變化
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//阻塞當前線程
//park並且檢查是否被中斷過
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
  • 當shouldParkAfterFailedAcquire返回成功,也就是前驅節點是Node.SIGNAL狀態時,進行真正的park將當前線程掛起。

setHeadAndPropagate(node, r)

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();
        }
    }
    
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
    
    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;
        }
    }
    
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)//注意這裏是從AQS隊列的尾節點開始查找的,
        //找到最後一個 waitStatus<=0 的那個節點,將其喚醒。
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}
  • 這段做的就是重新設置頭結點,判斷AQS隊列後面還有沒有等待被喚醒的線程,有的話繼續去喚醒。

cancelAcquire

  • acquireQueued方法在出現異常時,會執行cancelAcquire方法取消當前node的acquire操作。
private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;

    // 跳過中間CANCELLED狀態的節點
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    // 將node設置爲CANCELLED狀態
    node.waitStatus = Node.CANCELLED;

    // 如果當前節點是tail節點,則直接移除
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {//如果pred不是head節點並且是SIGNAL 狀態,
            //或者可以設置爲SIGNAL 狀態,
            //那麼將pred的next設置爲node.next,也就是移除當前節點
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);//喚醒node的後繼節點
        }

        node.next = node; // help GC
    }
}
private void unparkSuccessor(Node node) {
    //如果waitStatus爲負數,則將其設置爲0(允許失敗)
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //喚醒當前節點後面的節點。通常是緊隨的next節點,
    //但是當next被取消或者爲空,則從tail到node之間的所有節點,
    //往後往前查找直到找到一個waitStatus <=0的節點,將其喚醒unpark
    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)
        LockSupport.unpark(s.thread);
}
  • 總結一下:

1、設置thread變量爲空,並且設置狀態爲canceled

2、跳過中間的已經被取消的節點

3、如果當前節點是tail節點,則直接移除。否則:

4、如果其前驅節點不是head節點並且(前驅節點是SIGNAL狀態,或者可以被設置爲SIGNAL狀態),那麼將當前節點移除。否則通過LockSupport.unpark()喚醒node的後繼節點

2.3.1 我們再看看countDown()方法

 public void countDown() {
        sync.releaseShared(1);
    }
    
     public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//state減一,返回state是否等於0
           //如果等於0,開始喚醒線程
            doReleaseShared();
            return true;
        }
        return false;
    }
           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;
            }
        }
  • 首先判斷state是否已經等於0,等於0,就調用doReleaseShared()方法開始喚醒第一個線程,至於doReleaseShared()方法,上面有提到,忘記可以往上翻,就在重置頭結點喚醒下一個結點那一塊。
  • 如果不等於0,就state減一,然後再判斷state是否已經等於0,等於0,就調用doReleaseShared()方法,不等於0,結束。

【完】

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