CountDownLatch源碼解析以及使用方法

CountDownLatch的原理以及使用方法

概述

​ 1 ountDownLatch這個類使一個線程等待其他線程各自執行完畢後再執行。

​ 2 是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢後,計數器的值就-1,當計數器的值爲0時,表示所有線程都執行完畢,然後在閉鎖上等待的線程就可以恢復工作了。

源碼解析

構造函數

當我們調用CountDownLatch countDownLatch=new CountDownLatch(4) 時候,此時會創建一個AQS的同步隊列,並把創建CountDownLatch 傳進來的計數器賦值給AQS隊列的 state,所以state的值也代表CountDownLatch所剩餘的計數次數

//參數count爲計數值
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);//創建同步隊列,並設置初始計數器值
}

await()

創建一個節點,加入到AQS阻塞隊列,並同時把當前線程掛起。

//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException {
	sync.acquireSharedInterruptibly(1);
}

//和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
	return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

acquireSharedInterruptibly(int arg)

//1.判斷當前線程是否有被中斷
//2.如果沒有的話,就調用tryAcquireShared(int acquires)方法,判斷當前線程是否還需要“阻塞”。
 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

doAcquireSharedInterruptibly(int arg)

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    	//加入等待隊列 				      
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
    	// 進入 CAS 循環
        try {
            for (;;) {
                //當一個節點(關聯一個線程)進入等待隊列後, 獲取此節點的 prev 節點 
                final Node p = node.predecessor();
                // 如果獲取到的 prev 是 head,也就是隊列中第一個等待線程
                if (p == head) {
                    // 再次嘗試申請 反應到 CountDownLatch 就是查看是否還有線程需要等待(state是否爲0)
                    int r = tryAcquireShared(arg);
                    // 如果 r >=0 說明 沒有線程需要等待了 state==0
                    if (r >= 0) {
                        //嘗試將第一個線程關聯的節點設置爲 head 
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //經過自旋tryAcquireShared後,state還不爲0,就會到這裏,第一次的時候,waitStatus是0,那麼node的waitStatus就會被置爲SIGNAL,第二次再走到這裏,就會用LockSupport的park方法把當前線程阻塞住
                //重組雙向鏈表,清空無效節點,掛起當前線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
 }

總結

1 當前線程就會進入了一個死循環當中,在這個死循環裏面,會不斷的進行判斷

2 通過調用tryAcquireShared方法,不斷判斷我們上面說的那個計數器,

3 看看它的值是否爲0了(爲0的時候,其實就是我們調用了足夠多次數的countDownLatch.countDown()方法的時候)

4 如果是爲0的話,tryAcquireShared就會返回1,設置第一個線程關聯的借點未head,然後跳出了循環,不再“阻塞”當前線程了。

5 需要注意的是,說是在不停的循環,其實也並非在不停的執行for循環裏面的內容,因爲在後面調用parkAndCheckInterrupt()方法時,在這個方法裏面是會調用 LockSupport.park(this)來禁用當前線程的。

parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

addWaiter(Node mode)

將當前線程加入等待隊列

static final Node SHARED = new Node();

static final Node EXCLUSIVE = null;

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 嘗試快速入隊操作,因爲大多數時候尾節點不爲 null
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
    	//如果尾節點爲空(也就是隊列爲空) 或者嘗試CAS入隊失敗(由於併發原因),進入enq方法
        enq(node);
        return node;
}

步驟:

1.構造Node實體,參數爲當前線程和mode,mode是SHARED 或者 EXCLUSIVE

2.嘗試快速入隊列操作

3.如果尾節點爲空(也就是隊列爲空) 或者嘗試CAS入隊失敗(由於併發原因),進入enq方法

enq(final Node node)

private Node enq(final Node node) {
    	// 死循環+CAS保證所有節點都入隊
        for (;;) {
            Node t = tail;
            // 如果隊列爲空 設置一個空節點作爲 head
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //加入隊尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
 }

說明:

1.死循環+CAS 樂觀鎖

2.CAS是實現原子性,compareAndSetTail底層調用的是unsafe類,直接操作內存,在cpu層上加鎖,直接對內存進行操作

setHeadAndPropagate(node, r)

private void setHeadAndPropagate(Node node, int propagate) {
    //備份head
    Node h = head; 
    //搶到鎖的線程被喚醒 將這個節點設置爲head
    setHead(node);
    // propagate 一般都會大於0 或者存在可被喚醒的線程
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
         // 只有一個節點 或者是共享模式 釋放所有等待線程 各自嘗試搶佔鎖
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

Node 中的waitStatus四種狀態

//線程已被 cancelled ,這種狀態的節點將會被忽略,並移出隊列
static final int CANCELLED =  1;
// 表示當前線程已被掛起,並且後繼節點可以嘗試搶佔鎖
static final int SIGNAL    = -1;
//線程正在等待某些條件
static final int CONDITION = -2;
//共享模式下 無條件所有等待線程嘗試搶佔鎖
static final int PROPAGATE = -3;

countDown()

//將計數值減一
//遞減鎖重入次數,當state=0時喚醒所有阻塞線程
public void countDown() {
        sync.releaseShared(1);
}

releaseShared(1)

// AQS類
public final boolean releaseShared(int arg) {
    	// arg 爲固定值 1
    	// 如果計數器state 爲0 返回true,前提是調用 countDown() 之前不能已經爲0
        if (tryReleaseShared(arg)) {
            // 喚醒等待隊列的線程
            doReleaseShared();
            return true;
        }
        return false;
}

// CountDownLatch 重寫的方法
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
    		// 依然是循環+CAS配合 實現計數器減1
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
}

/// AQS類
 private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                // 如果節點狀態爲SIGNAL,則他的next節點也可以嘗試被喚醒
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);//成功則喚醒線程
                }
                // 將節點狀態設置爲PROPAGATE,表示要向下傳播,依次喚醒
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
}

總結

1、AQS 分爲獨佔模式和共享模式,CountDownLatch 使用了它的共享模式。

2、AQS 當第一個等待線程(被包裝爲 Node)要入隊的時候,要保證存在一個 head 節點,這個 head 節點不關聯線程,也就是一個虛節點。

3、當隊列中的等待節點(關聯線程的,非 head 節點)搶到鎖,將這個節點設置爲 head 節點。

4、第一次自旋搶鎖失敗後,waitStatus 會被設置爲 -1(SIGNAL),第二次再失敗,就會被 LockSupport 阻塞掛起。

5、如果一個節點的前置節點爲 SIGNAL 狀態,則這個節點可以嘗試搶佔鎖。

實現邏輯

1、初始化CountDownLatch實際就是設置了AQS的state爲計數的值

2、調用CountDownLatch的countDown方法時實際就是調用AQS的釋放同步狀態的方法,每調用一次就自減一次state值

3、調用await方法實際就調用AQS的共享式獲取同步狀態的方法acquireSharedInterruptibly(1),這個方法的實現邏輯就調用子類Sync的tryAcquireShared方法,只有當子類Sync的tryAcquireShared方法返回大於0的值時纔算獲取同步狀態成功,否則就會一直在死循環中不斷重試,直到tryAcquireShared方法返回大於等於0的值,而Sync的tryAcquireShared方法只有當AQS中的state值爲0時纔會返回1,否則都返回-1,也就相當於只有當AQS的state值爲0時,await方法纔會執行成功,否則就會一直處於死循環中不斷重試。

示例

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        /*創建CountDownLatch實例,計數器的值初始化爲5*/
        final CountDownLatch downLatch = new CountDownLatch(5);

        /*每個線程等待1s,表示執行比較耗時的任務*/
        for(int i = 0;i < 5;i++){
            final int num = i;
            new Thread(new Runnable() {
                public void run() {
                    try {
                        Thread.sleep(1000);

                    }catch (InterruptedException e){
                        e.printStackTrace();

                    }

                    System.out.println(String.format("thread %d has finished",num));

                    /*任務完成後調用CountDownLatch的countDown()方法*/
                    downLatch.countDown();
                }
            }).start();
        }

        /*主線程調用await()方法,等到其他5個線程執行完後才繼續執行*/
        downLatch.await();

        System.out.println("all threads have finished,main thread will continue run");
    }
}

輸出

thread 1 has finished
thread 2 has finished
thread 0 has finished
thread 3 has finished
thread 4 has finished
all threads have finished,main thread will continue run

使用場景

1 需要等待某個條件達到要求後才能做後面的事情

2 同時當線程都完成後也會觸發事件,以便進行後面的操作

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

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