每日一博 - CountDownLatch使用场景分析以及源码分析

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日一博 - CountDownLatch使用场景分析以及源码分析相关的知识,希望对你有一定的参考价值。


并发编程常用的工具类简介

我们先看并发编程中提供的几个常用的工具类

  • CountDownLatch : CountDownLatch 用于阻塞当前 1 个或多个线程,其目的是让这些线程等待其它线程的执行完成。

    可以简单将其理解为一个计数器,当初始化一个 count=n 的 CountDownLatch 对象之后,需要调用该对象的 CountDownLatch#countDown 方法来对计数器进行减值,直到计数器为 0 的时候,等待该计数器的线程才能继续执行。

    但是需要注意的一点是,执行 CountDownLatch#countDown 方法的线程在执行完减值操作之后,并不会因此而阻塞。真正阻塞等待事件的是调用 CountDownLatch 对象 CountDownLatch#await 方法的线程,该线程一直会阻塞直到计数器计数变为 0 为止。

  • CyclicBarrier :CyclicBarrier 用于阻塞当前多个线程,其目的是让这些线程彼此之间相互等待,当这些线程均到达屏障后再一起往下执行

  • Semaphore:信号量,可以通过控制“许可证”的数量,来保证线程之间的配合

  • Phaser:和CyclicBarrier类似,但计数可变

  • Exchanger :两个线程交换对象

  • Condition : 可以控制线程的“等待”和“唤醒” , Object.wait()的升级版本


CountDownLatch 概述

CountDownLatch位于java.util.cucurrent包下,Java1.5被引入。同时被引入的还有其他几个工具类比如: CyclicBarrier、Semaphore、ConcurrenthashMap和BlockingQueue等。

  • CountDownLatch : A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

  • CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.

CountDownLatch 是一个同步计数器, 通常用于一个线程或者多个线程等待另外一组线程执行完成之前一直等待。

CountDownLatch在使用时,会初始化一个计数器,计数器的数量为线程的数量。 每一个线程执行完成以后,就会调用countDown()方法将计数器的个数-1 ,直到计数器的个为0 , 表示所有线程都执行完毕, 这个时候调用await()方法的等待线程就可以恢复继续工作了。


源码分析

可以看出 CountDownLatch是基于Sync类实现的,而Sync继承AQS, 是AQS共享模式


使用场景

使用场景一: 模拟高并发并发执行(让多个线程等待)

举个例子,我们打算200个并发同时去做一笔业务,我们如果使用代码该如何实现呢?

那简单呀,老哥。

我打算去模拟200并发去做业务,那我们知道CountDownLatch 是 等countDown() 逐个减一以后,直到为0, 调用await()的线程才去工作。

那我就模拟让这200个线程调用await() ,然后等 count=0的时候 一起搞业务呗。

MMP,让我想了公司到饭点,大家一起冲刺去干饭的场景 , 这个CountDownLatch 就是那墙壁上的钟表啊 ,滴答滴答 ,就等11:45分…

  • 我们每个人都做了准备动作(类似countDown())
  • 时间一到(就像那await()),我们每个干饭人就是那业务线程, 那可口的大餐就是那业务

来吧,show code

package com.artisan.juc;

import java.util.concurrent.CountDownLatch;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/5 1:42
 * @mark: show me the code , change the world
 */
public class CountDownLatchTest {


    public static void main(String[] args) throws InterruptedException {

        int cnt = 10;
        CountDownLatch countDownLatch = new CountDownLatch(cnt);

        // 模拟10个并发干饭
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    // 干饭人准备完毕……干饭人都阻塞在这,等待号令(cnt=0)
                    countDownLatch.await();
                    System.out.println("编号:" + Thread.currentThread().getName() + " 开始干饭...." + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            // 模拟一部分业务耗时
            Thread.sleep(10);
            // 每个线程调用一次,cnt每次减一,直到为0,就是干饭信号
            countDownLatch.countDown();
            System.out.println("countDown执行一次,个数:" + (--cnt));

        }
    }

}
    

可以看到,我们通过CountDownLatch#await()方法,让多个业务线程启动后阻塞等待, 等待主线程调用CountDownLatch#count() 方法将计数器减为0以后,让所有的业务线程一起并发运行,这样就实现了我们多个线程并发的目的。


使用场景二: 模拟异步执行后回到主线程的业务(让一个线程等待)

举个例子,我们有一些依赖关系的业务

String  str = method1();  // 耗时1S 
String  str2 = method2();// 耗时2S 

method3(str, str2); 

这mmp ,数据有依赖呀, 当然了,你可以用CompletableFuture优化,我们这里不讨论它嘛

我们看看CountDownLacth该怎么玩?

分析一下这个业务场景, 让method 1 和 method 2 异步执行,然后执行完以后再回到主线程,取到返回结果汇总呗

我们简化下,不获取返回结果

package com.artisan.juc;

import java.util.concurrent.*;

/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/11/5 1:42
 * @mark: show me the code , change the world
 */
public class CountDownLatchTest2 {


    public static void main(String[] args) throws InterruptedException {

        // 2 为线程个数
        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + " --执行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();




        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + "**执行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        
        countDownLatch.await();
        System.out.println("主线业务继续执行");

    }


}
    

我们可以看到: 在每个线程 完成的最后一行加上CountDownLatch#countDown(),让计数器-1;当所有线程完成-1,主线程之前阻塞在这里 countDownLatch.await(); ,直到计数器减到0后,往下继续执行自己的业务。

以上是关于每日一博 - CountDownLatch使用场景分析以及源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java Review - 并发编程_ CountDownLatch原理&源码剖析

每日一博 - Review线程池_02

Java Review - 并发编程_ CountDownLatch原理&源码剖析

每日一博 - 延时任务的多种实现方式解读

每日一博 - 使用环形队列实现高效的延时消息

每日一博 - DelayQueue阻塞队列源码解读