CountDownLatch 原理分析

以前文章

两个线程交替执行输出,一个数字1-10,一个字符a-e ,打印出来12a34b56c78d910e

ReentrantLock lock unLock 原理分析

Condition await signal 阻塞和唤醒 原理分析

CountDownLatch 原理分析

当我们需要实现并发请求,或者一个线程需要等待其他线程执行完成之后再执行时 ,我们可以使用 CountDownLatch

应用程序实例

public class CountDownLatchDemo extends Thread{

    static CountDownLatch countDownLatch = new CountDownLatch(2);
    public static void main(String[] args) {

        // 开启多个线程,并在run方法中进行线程等待,
        // 只有 countDown  归 0时才会进行执行,
        // 这种方式就是模拟并发执行
        for (int i = 0; i < 3; i++) {
            new CountDownLatchDemo().start();
        }
        // 当然 因为countDownLatch(2) ,所以 执行一次 countDown 还是不行的,
        // 真是情况下可以设置为1嘛  ,这里是例子合并成了一个
        countDownLatch.countDown();

        // 该线程是先执行业务逻辑, 最后在 countDown , 计数归0 ,
        // 场景有点类似,在该线程中预热缓存 ,在上面多个线程中读取缓存
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t"+"先执行我。。。。");
            countDownLatch.countDown();
        },"ThreadD").start();
    }

    @Override
    public void run() {
        try {
            // 在 countDown 计数器为归0 之前一直在这里阻塞
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+
                               "\t"+System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果

ThreadD 先执行我。。。。

ThreadA 1559284347551

ThreadB 1559284347551

ThreadC 1559284347551

类图关系

在这里插入图片描述

CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到 countDown 是倒数的意思,类似于我们倒计时的概念。CountDownLatch 提供了两个方法,一个是 countDown,一个是 await,CountDownLatch 初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过 countDown 来倒数。

疑问:

  1. await 等待的原理:CountDownLatchDemo 多个线程同时启动时,执行到 run() 后为什么会处于等待状态?
  2. countDown 唤醒等待的原理: 而 当 countDown 执行多次后 计数器归 0 后,是怎么通知 CountDownLatchDemo 线程的?

await

我们知道在初始化 CountDownLatch 的时候, 会传入一个整数,每次执行一次 countDown() ,计数器减一,当计算器为 0 时,处于等待状态的线程才会继续运行。计数器未归 0 的时候这块代码到底是做了什么呢?

// 根据所给的参数构造一个实例
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    // volatile int state = count;
    this.sync = new Sync(count);
}

从上面的构造可以知道, 初始化 CountDownLatch 的时候需要传入一个整数,该值最终被保存到 state 中。是volatile 修饰的,可以保证有序性和可见性。

// 导致当前线程等待,直到锁存器倒计数到零,
// 除非线程是被中断
public void await() throws InterruptedException {
    // 获得共享锁
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 如果线程被中断了,则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获得共享锁,
    // 如果 计数器为0 则返回1,否则返回-1
    if (tryAcquireShared(arg) < 0)
        // 表示当前计数器还未归0 ,需要去获得共享锁,
        // 将当前线程封装成node 节点添加到同步队列中,并再次获取锁
        // 如果计数器还未归0 则获取锁失败,
        // 并修改当前线程的上一个节点的waitStatus=SIGNAL(-1),
        // 并挂起当前线程
        doAcquireSharedInterruptibly(arg);
}
// 如果当前的 计数器 为0 返回1 否则返回-1
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 创建一个共享锁,封装当前线程的节点到同步队列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 拿到当前节点的上一个节点
            final Node p = node.predecessor();
            // 如果该当前线程的上一个节点 是 head 节点,则尝试去获得锁
            if (p == head) {
                // r=1 表示计数器已归0
                // r=-1 表示计数器还未归0
                int r = tryAcquireShared(arg);
                // 如果此时 正好CountDown 次数 使计数器归 0
                // 因为是自旋,当线程挂起后再次被唤醒后还是会执行到这块代码
                
                // todo 这块等分析完 countDown 之后再来分析
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 获得锁失败后,
            // 将该节点的上一个节点 waitStatus设置为-1,
            // 删除取消的节点
            // 如果当前节点的上一个节点 的waitStatus=-1时,返回true
            
            // 这个方法在分析 lock 方法时已经分析过了,
            // 主要做了一下功能
            // 获得锁失败后是否应该挂起
    		// CANCELLED =  1 : 取消状态
    		// SIGNAL    = -1 :只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
    		// CONDITION = -2 :在Condition中(await,signal)中 会使用到
   			// PROPAGATE = -3 :下一个acquireShared应该无条件传播
            // 1. 当 线程的pred线程被取消(1)时,会将该线程从竞争锁队列中删除
    		// 2. 当 线程的pred线程状态不是被取消(1)或者不是 -1 时,会将该线程的pred线程设置为 -1 :SIGNAL
    		// 3. 当 线程的pred线程状态是-1是,则返回true,表示可以进行挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                // park 当前线程
                // 如果返回true 表示已经中断
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在这里插入图片描述

countDown

该方法主要是将计数器归0 ,说是释放锁,其实在 await 中并没有获得锁,所以这个 “释放锁” 只是在思想上与unlock 相似。

// 计数器递减,如果计数达到零释放所有等待的线程,
public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 尝试释放共享锁
    // 实际上是将 计数器递减,如果递减后为0 ,则返回true
    if (tryReleaseShared(arg)) {
        // 如果在这里 计数器已经归0 了,便可以进行锁释放
        // 该方法主要是将 挂起的线程进行唤醒,修改waitStatus =-3 表示共享锁
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
    for (;;) {
        Node h = head;
        // 因为head 节点中并没有存储线程,
        // 如果head 和 tail 相同的话,说明同步队列中还没有等待获得锁的节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // head 的 waitStatus 如果 SIGNAL 表示可以去获取锁
            if (ws == Node.SIGNAL) {
                // 修改 head 的waitStatus =0 ,
                // 如果成功 唤醒head 的next 节点,
                // 如果失败 跳出当前循环:说明已经设置修改过了
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // unpark h.next
                // 唤醒 head节点的next节点
                // 这个方法在上面也已经分析过了,主要是唤醒当前节点的下一个节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     // 在上面if 的方法中,修改了head 的waitStatus =0 了,
                     // 这里在修改为 -3  ,表示是一个共享锁
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

在这里插入图片描述

await 被唤醒后的逻辑

从上面的 awaitcountDown 我们可以知道,根据实例化 CountDownLatch 时的构造参数的整数作为计数器。当执行 countDown 的时候计数器会递减,当计数器还未递减到0时,在await 方法中线程都会通过封装成一个node(thread=当前线程,waitStatus=0,nextWaiter=SHARED) 添加到同步队列中,当自旋获得不到锁时会将当前线程的上一个节点的waitStatus设置为-1,并挂起该线程。当 计数器递减为0 后, 会修改head节点的状态值为 waitStatus=PROPAGATE(-3),并唤醒head 节点的下一个节点。

此时我们接着分析,当挂起的线程被唤醒后又做了哪些事情。继续在 await 方法中的 doAcquireSharedInterruptibly(1) 来分析。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 创建一个共享锁,封装当前线程的节点到同步队列中
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 拿到当前节点的上一个节点
            final Node p = node.predecessor();
            // 如果该当前线程的上一个节点 是 head 节点,则尝试去获得锁
            if (p == head) {
                // r=1 表示计数器已归0
                // r=-1 表示计数器还未归0
                int r = tryAcquireShared(arg);
                // 如果此时 正好CountDown 次数 使计数器归 0
                // 因为是自旋,当线程挂起后再次被唤醒后还是会执行到这块代码
                if (r >= 0) {
                    // todo xxxx
                    // 设置当前节点为 head
                    setHeadAndPropagate(node, r);
                    // 并取消原head 的next 引用
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 获得锁失败后,
            // ..........
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
// 设置新的 head 节点,
// 此时是从 CountDownLatch 过来的,propagate 的值只能为 1 (即计数器归0)才会走到这里
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    // 设置新的 head 节点,
    setHead(node);
    /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
    // propagate > 0 表示当前计数器已归0
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 获得当前节点的下一个节点,如果是共享锁继续进行唤醒
        Node s = node.next;
        // 此时 CountDownLatch 过来的节点 是 nextWaiter = SHARED;
        // 所以是满足的,即共享锁,继续唤醒下一个节点
        if (s == null || s.isShared())
            // 此时head 节点已经更新了,
            // 所以继续执行该代码,会继续唤醒该节点的下个节点的线程
            doReleaseShared();
    }
}

在这里插入图片描述

疑问

  1. await 等待的原理:CountDownLatchDemo 多个线程同时启动时,执行到 run() 后为什么会处于等待状态?

答:当执行了 await 方法后,由于计数器不归0 ,尝试获得锁会失败。并将该线程添加到同步队列中(node=(thread=null,waitStatus=0,nextWaiter=SHARED)) 是一个标志共享锁的节点。并再次节点中获取锁,失败后修改该节点的上一个节点的状态 waitStatus=-1 ,并挂起当前线程。

  1. countDown 唤醒等待的原理: 而 当 countDown 执行多次后 计数器归 0 后,是怎么通知 CountDownLatchDemo 线程的?

答:当执行完 await 后,由于计数器还未归0 ,所有线程当添加到同步队列中,当执行了 countDown 方法使计数器归0后会进行锁的释放。锁的释放主要是将head节点的 waitStatus=-3(共享) 并唤醒处于 head 节点的next 节点的线程(ThreadA)让其去竞争锁。被唤醒的线程再次回在 await 方法中获取锁,此时计数器归0 ,获得锁成功,该节点成为新的 head 。因为节点是共享锁,所以会再次调用锁的释放,再次去唤醒新 head 的next 节点,直至所有挂起的线程都获得锁。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值