首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【原创】Java并发编程系列22 | 倒计时器CountDownLatch

【原创】Java并发编程系列22 | 倒计时器CountDownLatch

作者头像
java进阶架构师
发布2020-06-04 17:49:18
7510
发布2020-06-04 17:49:18
举报
文章被收录于专栏:Java进阶架构师Java进阶架构师

20大进阶架构专题每日送达

并发编程中常遇到这种情况,一个线程需要等待另外多个线程执行后再执行。遇到这种情况你一般怎么做呢?今天就介绍一种JDk提供的解决方案来优雅的解决这一问题,那就是倒计时器CountDownLatch。本文将分以下两部分介绍:

  1. CountDownLatch的使用
  2. CountDownLatch源码分析

1. CountDownLatch的使用

CountDownLatch的作用是让线程等待其它线程完成一组操作后才能执行,否则就一直等待。

举个开会的例子:

  1. 老板先进入会议室准备材料
  2. 等待5个员工陆续进入会议室
  3. 员工到齐开始开会

老板不能一来就开会,必须要等员工都到了再开会,用CountDownLatch实现如下:

public class CountDownLatchTest {
    private static CountDownLatch countDownLatch = new CountDownLatch(5);

    // Boss线程,等待员工到齐开会
    static class BossThread extends Thread {
        @Override
        public void run() {
            System.out.println("Boss进入会议室准备材料...");
            System.out.println("Boss在会议室等待...");
            try {
                countDownLatch.await(); // Boss等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Boss等到人齐了,开始开会...");
        }
    }

    // 员工到达会议室
    static class EmpleoyeeThread extends Thread {
        @Override
        public void run() {
            System.out.println("员工" + Thread.currentThread().getName()
                    + ",到达会议室....");
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) {
        // Boss线程启动
        new BossThread().start();

        // 员工到达会议室
        for (int i = 0; i < countDownLatch.getCount(); i++) {
            new EmpleoyeeThread().start();
        }
    }
}

控制台输出:

Boss进入会议室准备材料...
Boss在会议室等待...
员工Thread-2,到达会议室....
员工Thread-3,到达会议室....
员工Thread-4,到达会议室....
员工Thread-1,到达会议室....
员工Thread-5,到达会议室....
Boss等到人齐了,开始开会...

总结CountDownLatch的使用步骤:(比如线程A需要等待线程B和线程C执行后再执行)

  1. 创建CountDownLatch对象,设置要等待的线程数N(这里是2);
  2. 等待线程A调用await()挂起;
  3. 线程B执行后调用countDown(),使N-1;线程C执行后调用countDown(),使N-1;
  4. 调用countDown()后检查N=0了,唤醒线程A,在await()挂起的位置继续执行。

2. CountDownLatch源码分析

CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。

2.1 类结构

CountDownLatch只有一个属性Sync,Sync是继承了AQS的内部类。

创建CountDownLatch时传入一个count值,count值被赋值给AQS.state

CountDownLatch是通过AQS共享锁实现的,AQS这篇文章中详细讲解了AQS独占锁的原理,AQS共享锁和独占锁原理只有很细微的区别,这里大致介绍下:

  • 线程调用acquireSharedInterruptibly()方法获取不到锁时,线程被构造成结点进入AQS阻塞队列。
  • 当有线程调用releaseShared()方法将当前线程持有的锁彻底释放后,会唤醒AQS阻塞队列中等锁的线程,如果AQS阻塞队列中有连续N个等待共享锁的线程,就将这N个线程依次唤醒
public class CountDownLatch {
    private static final class Sync extends AbstractQueuedSynchronizer {
        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);
    }
}
2.2 await()

await()是将当前线程阻塞,理解await()的原理就是要弄清楚await()是如何将线程阻塞的。

await()调用的就是AQS获取共享锁的方法。当AQS.state=0时才能获取到锁,由于创建CountDownLatch时设置了state=count,此时是获取不到锁的,所以调用await()的线程挂起并构造成结点进入AQS阻塞队列。

创建CountDownLatch时设置AQS.state=count,可以理解成锁被重入了count次。await()方法获取锁时锁被占用了,只能阻塞。

/**
 * CountDownLatch.await()调用的就是AQS获取共享锁的方法acquireSharedInterruptibly()
 */
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

/**
 * 获取共享锁
 * 如果获取锁失败,就将当前线程挂起,并将当前线程构造成结点加入阻塞队列
 * 判断是否获取锁成功的方法由CountDownLatch的内部类Sync实现
 */
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)          // 尝试获取锁的方法由CountDownLatch的内部类Sync实现
        doAcquireSharedInterruptibly(arg);  // 获取锁失败,就将当前线程挂起,并将当前线程构造成结点加入阻塞队列
}

/**
 * CountDownLatch.Sync实现AQS获取锁的方法
 * 只有AQS.state=0时获取锁成功。
 * 创建CountDownLatch时设置了state=count,调用await()时state不为0,返回-1,表示获取锁失败。
 */
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
2.3 countDown()

countDown()方法是将count-1,如果发现count=0了,就唤醒阻塞的线程。

countDown()调用AQS释放锁的方法,每次将state减1。当state减到0时是无锁状态了,就依次唤醒AQS队列中阻塞的线程来获取锁,继续执行逻辑代码。

/**
 *  CountDownLatch.await()调用的就是AQS释放共享锁的方法releaseShared()
 */
public void countDown() {
    sync.releaseShared(1);
}

/**
 * 释放锁
 * 如果锁被全部释放了,依次唤醒AQS队列中等待共享锁的线程
 * 锁全部释放指的是同一个线程重入了N次需要N次解锁,最终将state变回0
 * 具体释放锁的方法由CountDownLatch的内部类Sync实现
 */
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 释放锁,由CountDownLatch的内部类Sync实现
        doReleaseShared();       // 锁全部释放之后,依次唤醒等待共享锁的线程
        return true;
    }
    return false;
}

/**
 * CountDownLatch.Sync实现AQS释放锁的方法
 * 释放一次,将state减1
 * 如果释放之后state=0,表示当前是无锁状态了,返回true
 */
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        // state每次减1
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;// state=0时,无锁状态,返回true
    }
}

总结

CountDownLatch用于一个线程A需要等待另外多个线程(B、C)执行后再执行的情况。

创建CountDownLatch时设置一个计数器count,表示要等待的线程数量。线程A调用await()方法后将被阻塞,线程B和线程C调用countDown()之后计数器count减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程A继续执行了。

CountDownLatch是由AQS实现的,创建CountDownLatch时设置计数器count其实就是设置AQS.state=count,也就是重入次数。await()方法调用获取锁的方法,由于AQS.state=count表示锁被占用且重入次数为count,所以获取不到锁线程被阻塞并进入AQS队列。countDown()方法调用释放锁的方法,每释放一次AQS.state减1,当AQS.state变为0时表示处于无锁状态了,就依次唤醒AQS队列中阻塞的线程来获取锁,继续执行逻辑代码。

参考资料

  1. 《Java并发编程之美》
  2. 《Java并发编程实战》
  3. 《Java并发编程的艺术》

并发系列文章汇总

【原创】01|开篇获奖感言 【原创】02|并发编程三大核心问题 【原创】03|重排序-可见性和有序性问题根源 【原创】04|Java 内存模型详解 【原创】05|深入理解 volatile 【原创】06|你不知道的 final 【原创】07|synchronized 原理 【原创】08|synchronized 锁优化 【原创】09|基础干货 【原创】10|线程状态 【原创】11|线程调度 【原创】12|揭秘 CAS 【原创】13|LockSupport 【原创】14|AQS 源码分析 【原创】15|重入锁 ReentrantLock 【原创】16|公平锁与非公平锁 【原创】17|读写锁八讲(上) 【原创】18|读写锁八讲(下) 【原创】19|JDK8新增锁StampedLock 【原创】20|StampedLock源码解析

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java进阶架构师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. CountDownLatch的使用
  • 2. CountDownLatch源码分析
    • 2.1 类结构
      • 2.2 await()
        • 2.3 countDown()
        • 总结
          • 参考资料
          • 并发系列文章汇总
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档