【線程】CountDownLatch 內部原理(十三)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、概念

  1. CountDownLatch 理解爲:手動倒計時。(輸入一個整數,然後每次-1,直到state = 0,退出)
    也叫做閉鎖,在完成某些運算是,只有其他所有線程的運算全部完成,當前運算才繼續執行。
    可以用於計算數量,平均值,等待線程退出等等等等。

=== 點擊查看top目錄 ===

二、主要方法

  1. CountDownLatch#countDown 遞減1
  2. CountDownLatch#await() 共享鎖阻塞等待 state = 0

=== 點擊查看top目錄 ===

三、源碼分析

爲了便於理解,建議先讀一下我的另一篇文章,介紹AQS 【線程】ReentrantLock 源碼剖析 (八)

=== 點擊查看top目錄 ===

3.1 整體架構

package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatch {
	// 繼承 AQS 的Sync
    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) { //嘗試進行加鎖 ,getState() == 0表示當前空閒,沒人佔據鎖,我可以拿到
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) { //嘗試釋放鎖,這個 releases參數沒鳥用
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState(); //獲取狀態 
                if (c == 0) //是0的話,說明沒有鎖了,那麼就釋放失敗,返回 false 
                    return false;
                int nextc = c-1;  //每次釋放 1 
                if (compareAndSetState(c, nextc))  //CAS 設置狀態,不成功就for自旋,必須設置成功爲止
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //等待條件滿足被喚醒

    public boolean await(long timeout, TimeUnit unit) //等待條件滿足被喚醒 (條件加了個超市)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public void countDown() { sync.releaseShared(1); } // -1操作,拿掉一個鎖

    public long getCount() {  return sync.getCount(); } // 獲取鎖數量

    public String toString() {  return super.toString() + "[Count = " + sync.getCount() + "]"; }
}

=== 點擊查看top目錄 ===

3.2 CountDownLatch 初始化

final CountDownLatch latch = new CountDownLatch(2);
  • 看下構造方法 CountDownLatch(int count)
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
    setState(count);
}
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
	private volatile int state; // 當前持有鎖的數量,0表示沒人持有鎖

    protected final void setState(int newState) {
        state = newState;
    }
    ...
}

初始化的目的就是:套N個鎖上去,然後一個個拿掉。

=== 點擊查看top目錄 ===

3.3 await 剖析

java.util.concurrent.CountDownLatch#sync

  • await 方法
public void CountDownLatch#await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
  • acquireSharedInterruptibly 方法
public final void AbstractQueuedSynchronizer#acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();  // 被打斷就直接拋異常了
    if (tryAcquireShared(arg) < 0) // -1 沒拿到鎖,1 拿到了鎖。
        doAcquireSharedInterruptibly(arg);  // 拿到鎖才進入這個方法
}
  • tryAcquireShared 方法
protected int Sync#tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;  // -1 沒拿到鎖,1 拿到了鎖。
}
  • doAcquireSharedInterruptibly 方法

下面的 addWaiter 方法建議看一下文章 【線程】ReentrantLock 源碼剖析 (八)


// 跑到這個方法,說明沒拿到鎖
private void AbstractQueuedSynchronizer#doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED); //注意此處,加入的是 共享鎖 ,addWaiter 方法,看一下之前的文章
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) { //喚醒,是喚醒 同步隊列的第一個
                int r = tryAcquireShared(arg);
                if (r >= 0) { // -1 沒拿到鎖,1 拿到了鎖。
                    setHeadAndPropagate(node, r); // 這個是重點!!!這個會喚醒下一個持有共享鎖的線程
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 全部阻塞在這裏,被喚醒一個個醒過來,第一個NodeNew後的thread最先醒過來,
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • setHeadAndPropagate 方法
private void AbstractQueuedSynchronizer#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(); // 這裏是重點啊!!!共享鎖直接進來這個方法
    }
}

---

final boolean AbstractQueuedSynchronizer.Node#isShared() {
    return nextWaiter == SHARED;
}

private void AbstractQueuedSynchronizer#setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 看下 doReleaseShared 方法
private void AbstractQueuedSynchronizer#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;
    }
}

注意:await 方法被notify之後,會去 notify 下一個持有共享鎖share lock 的thread,具體看 方法setHeadAndPropagate

doReleaseShared 方法調用場景
  1. countDown 的時候調用
  2. thread被notify之後,調用setHeadAndPropagate之後再調用

=== 點擊查看top目錄 ===

3.4 countDown 剖析

  • countDown 方法
public void CountDownLatch#countDown() {
    sync.releaseShared(1);  // 釋放共享鎖
}
  • releaseShared 方法
public final boolean AbstractQueuedSynchronizer#releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
    	// 能進來這裏,說明 getState() 已經等於 0 了,也就是沒有線程持有鎖了,這是 countDown 的最後一次。
        doReleaseShared(); //釋放操作
        return true;
    }
    return false;
}
  • tryReleaseShared 方法
protected boolean Sync#tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) { // 自旋CAS
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0; //等於0的話,返回true,就要叫醒同步隊列的線程了
    }
}

====== doReleaseShared方法 ====== 查看

=== 點擊查看top目錄 ===


看源碼一般都能看懂,看不懂就往下走看下調試剖析。

四、場景分析

  1. 多thread await阻塞,等待被喚醒
  2. CountDown到0,開始喚醒

1. 多thread await阻塞,等待被喚醒

結構如下:

在這裏插入圖片描述

代碼:

public class _05_02_TestCountDownLatch {
    public static void main(String[] args) throws Exception {
        System.out.println(Thread.currentThread().getName());
        final CountDownLatch latch = new CountDownLatch(2);

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                Scanner sc = new Scanner(System.in);
                System.out.println("點擊任意鍵終止線程 ...");
                sc.nextLine();
                latch.countDown();
            }, "A_" + i).start();
        }

        // sleep 保證上面先執行
        Thread.sleep(1000);

        // 多個線程await等待,模擬排隊
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    latch.await();
                    System.out.println(Thread.currentThread().getName() + " be notified .");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"C_" + i).start();
        }

        // sleep 保證上面先執行
        Thread.sleep(1000);
        System.out.println("Main latch.await()... ");
        // 這個地方 latch.await(); 目的是爲了查看排隊狀態
        latch.await();
        System.out.println("main end");
    }
}

輸出:

main
點擊任意鍵終止線程 ...
點擊任意鍵終止線程 ...


C_0 be notified .
C_4 be notified .
C_3 be notified .
C_2 be notified .
C_1 be notified .
Main latch.await()... 
main end

斷點位置:(現在就先打一個地方,然後逐漸 Step Into 進去)
在這裏插入圖片描述
expression:Thread.currentThread().getName().equals(“main”)

斷點停下來的位置,位於 main 方法的 latch.await() 位置的下一步,如下圖:
在這裏插入圖片描述

  • 開始看調試分析:
    在這裏插入圖片描述
  1. state = 2 說明有線程持有鎖
  2. nextWaiter的作用僅僅爲了判斷 s.isShared() . 使用的地方: 方法setHeadAndPropagate
  3. 說明head節點只是個NodeNew, 具體看doAcquireSharedInterruptibly#addWaiter(Node.SHARED)
  4. exclusiveOwnerThread = null,說明沒有獨佔鎖,那麼前面那個2是共享鎖。我們在代碼 doAcquireSharedInterruptibly#addWaiter(Node.SHARED) 也可以知道加的是Shared鎖。
  5. 一排next,說明都在阻塞排隊

=== 看下結構圖 ===

=== 點擊查看top目錄 ===

2. CountDown到 0,開始喚醒

  • Countdownlatch 喚醒的代碼,我就不貼了。下面看第一個thread被喚醒後,是怎麼把同步鏈表的Node逐個喚醒的。
  1. 喚醒第一個之後,進入 方法setHeadAndPropagate ,setHead(node); 方法讓head 右移。
    在這裏插入圖片描述

  2. 看下圖,第一次喚醒的肯定是第一個節點 head的下一個

  3. CountDownlatch 到最後,肯定釋放了全部鎖,那麼 tryAcquireShared() = 1,進入 setHeadAndPropagate 方法

doAcquireSharedInterruptibly
3. setHeadAndPropagate 方法 doReleaseShared() 釋放下一個節點。
setHeadAndPropagate

=== 點擊查看top目錄 ===

五、番外篇

下一章節:【線程】ThreadLocal 剖析 (十四)
上一章節:【線程】線程八鎖與Synchronzied內部原理(十二)

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