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

2021-03-15 04:33

阅读:646

标签:调度   使用步骤   如何   区别   复习   port   总结   java并发编程   bat   

【原创】Java并发编程系列22 | 倒计时器CountDownLatch
收录于话题
#进阶架构师 | 并发编程专题
12个

点击上方“java进阶架构师”,选择右上角“置顶公众号”
20大进阶架构专题每日送达
技术图片
技术图片
并发编程中常遇到这种情况,一个线程需要等待另外多个线程执行后再执行。遇到这种情况你一般怎么做呢?今天就介绍一种JDk提供的解决方案来优雅的解决这一问题,那就是倒计时器CountDownLatch。本文将分以下两部分介绍:
CountDownLatch的使用
CountDownLatch源码分析

1. CountDownLatch的使用


CountDownLatch的作用是让线程等待其它线程完成一组操作后才能执行,否则就一直等待。
举个开会的例子:
老板先进入会议室准备材料
等待5个员工陆续进入会议室
员工到齐开始开会
老板不能一来就开会,必须要等员工都到了再开会,用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 

控制台输出:


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

总结CountDownLatch的使用步骤:(比如线程A需要等待线程B和线程C执行后再执行)
创建CountDownLatch对象,设置要等待的线程数N(这里是2);
等待线程A调用await()挂起;
线程B执行后调用countDown(),使N-1;线程C执行后调用countDown(),使N-1;
调用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 

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) 

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队列中阻塞的线程来获取锁,继续执行逻辑代码。
参考资料
《Java并发编程之美》
《Java并发编程实战》
《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源码解析

之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。
《java面试宝典5.0》(初中级)
《350道Java面试题:整理自100+公司》(中高级)
《资深java面试宝典-视频版》(资深)
《Java[BAT]面试必备》(资深)
分别适用于初中级,中高级,资深级工程师的面试复习。
内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。
技术图片
获取方式:点“在看”,V信关注上述单号并回复 【面试】即可领取,更多精彩陆续奉上。

看到这里,证明有所收获
必须点个在看支持呀,喵

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

标签:调度   使用步骤   如何   区别   复习   port   总结   java并发编程   bat   

原文地址:https://blog.51cto.com/15009303/2552595


评论


亲,登录后才可以留言!