前言 - CountDownLatch 是什么?
CountDownLatch 是具有 synchronized 机制的一个工具, 目的是让一个或者多个线程等待, 直到其他线程的一系列操作完成.
CountDownLatch 初始化的时候, 需要提供一个整形数字, 数字代表着线程需要调用 countDown() 方法的次数, 当计数为 0 时, 线程才会继续执行 await() 方法后的其他内容.
CountDownLatch(int count);
对象中的方法
getCount:
返回当前的计数 count 值,
public void countDown()
调用此方法后, 会减少计数 count 的值.
递减后如果为 0, 则会释放所有等待的线程
- public void await()
- throws InterruptedException
调用 CountDownLatch 对象的 await 方法后.
会让当前线程阻塞, 直到计数 count 递减至 0.
如果当前线程数大于 0, 则当前线程在线程调度中将变得不可用, 并处于休眠状态, 直到发生以下两种情况之一:
1, 调用 countDown() 方法, 将计数 count 递减至 0.
2, 当前线程被其他线程打断.
- public boolean await(long timeout,
- TimeUnit unit)
- throws InterruptedException
同时 await 还提供一个带参数和返回值的方法.
如果计数 count 正常递减, 返回 0 后, await 方法会返回 true 并继续执行后续逻辑.
或是, 尚未递减到 0, 而到达了指定的时间间隔后, 方法返回 false.
如果时间小于等于 0, 则此方法不执行等待.
实际案例
join 阻塞等待线程完成
首先建立 3 个线程.
- public class Worker1 implements Runnable {
- @Override
- public void run() {
- System.out.println("- 线程 1 启动");
- try {
- Thread.sleep(13_000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程 1 完成 -- 我休眠 13 秒 \ r\n");
- }
- }
- public class Worker2 implements Runnable {
- @Override
- public void run() {
- System.out.println("- 线程 2 启动");
- try {
- Thread.sleep(3_000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程 2 完成 -- 我休眠 3 秒 \ r\n");
- }
- }
- public class Worker3 implements Runnable {
- @Override
- public void run() {
- System.out.println("- 线程 3 启动");
- try {
- Thread.sleep(3_000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- try {
- Thread.sleep(3_000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("线程 3 完成 -- 我休眠 6 秒 \ r\n");
- System.out.println();
- }
- }
- public class Main {
- public static void main(String[] args) throws InterruptedException {
- Worker1 worker1 = new Worker1();
- Worker2 worker2 = new Worker2();
- Worker3 worker3 = new Worker3();
- Thread thread1 = new Thread(worker1,"线程 1");
- Thread thread2 = new Thread(worker2,"线程 2");
- Thread thread3 = new Thread(worker3,"线程 3");
- thread1.start();
- thread2.start();
- thread3.start();
- thread1.join();
- thread2.join();
- thread3.join();
- System.out.println("主线程结束....");
- }
- }
打印结果如下:
- 线程 3 启动
- 线程 2 启动
- 线程 1 启动
线程 2 完成 -- 我休眠 3 秒
线程 3 完成 -- 我休眠 6 秒
线程 1 完成 -- 我休眠 13 秒
主线程结束....
Process finished with exit code 0
可以看出三个线程是并行执行的. 启动顺序, 并不和执行完毕的顺序一致, 但可以明确的是, 主线程为一直阻塞, 直到三个线程执行完毕.
CountDownLatch 用法
阿里巴巴的数据库连接池 Druid 中也用了 countDownLatch 来保证初始化.
- // 开启创建连接的线程, 如果线程池 createScheduler 为 null,
- // 则开启单个创建连接的线程
- createAndStartCreatorThread();
- // 开启销毁过期连接的线程
- createAndStartDestroyThread();
自己编写一个例子:
这里模拟一种情况:
主线程 依赖 线程 A 初始化三个数据, 才能继续加载后续逻辑.
- public class CountDownArticle {
- /**
- * 模拟 主线程 依赖 线程 A 初始化一个数据, 才能继续加载后续逻辑
- */
- public static void main(String[] args) throws InterruptedException {
- AtomicReference<String> key = new AtomicReference<>("");
- CountDownLatch countDownLatch = new CountDownLatch(3);
- Thread t = new Thread(() -> {
- try {
- // 休眠 5 秒, 模拟数据的初始化
- TimeUnit.SECONDS.sleep(5);
- key.set("核心秘钥 123456");
- System.out.println("数据 1 初始化完毕");
- // 释放 --- 此处可以在任何位置调用, 很灵活
- countDownLatch.countDown();
- System.out.println("数据 2 初始化完毕");
- countDownLatch.countDown();
- System.out.println("数据 3 初始化完毕");
- countDownLatch.countDown();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- t.start();
- // 等待数据初始化, 阻塞
- countDownLatch.await();
- System.out.println("key:" + key.get());
- }
- }
打印内容如下:
数据 1 初始化完毕
数据 2 初始化完毕
数据 3 初始化完毕
key: 核心秘钥 123456
CountDownLatch 和 Join 用法的区别?
在使用 join() 中, 多个线程只有在执行完毕之后欧才能被解除阻塞, 而在 CountDownLatch 中, 线程可以在任何时候任何位置调用 countdown 方法减少计数, 通过这种方式, 我们可以更好地控制线程的解除阻塞, 而不是仅仅依赖于连接线程的完成.
join() 方法的执行逻辑如下图所示:
原理
从源码可以看出, CountDownLatch 是依赖于 AbstractQueuedSynchronizer 来实现这一系列逻辑的.
队列同步器 AbstractQueuedSynchronizer
是一个用来构建锁和同步器的框架, 它在内部定义了一个被标识为 volatile 的名为 state 的变量, 用来表示同步状态.
多个线程之间可以通过 AQS 来独占式或共享式的抢占资源.
并且它通过内置的 FIFO 队列来完成线程的排队工作.
CountDownLatch 中的 Sync 会优先尝试修改 state 的值, 来获取同步状态. 例如, 如果某个线程成功的将 state 的值从 0 修改为 1, 表示成功的获取了同步状态. 这个修改的过程是通过 CAS 完成的, 所以可以保证线程安全.
反之, 如果修改 state 失败, 则会将当前线程加入到 AQS 的队列中, 并阻塞线程.
总结
CountDownLatch(int N) 中的计数器, 可以让我们支持最多等待 N 个线程的操作完成, 或是一个线程操作 N 次.
如果仅仅只需要等待线程的执行完毕, 那么 join 可能就能满足. 但是如果需要灵活的控制线程, 使用 CountDownLatch.
注意事项
countDownLatch.countDown();
这一句话尽量写在 finally 中, 或是保证此行代码前的逻辑正常运行, 因为在一些情况下, 出现异常会导致无法减一, 然后出现死锁.
CountDownLatch 是一次性使用的, 当计数值在构造函数中初始化后, 就不能再对其设置任何值, 当 CountDownLatch 使用完毕, 也不能再次被使用.
写在最后
为了方便大家学习讨论, 我创建了一个 java 疑难攻坚互助大家庭, 和其他传统的学习交流不同. 本群主要致力于解决项目中的疑难问题, 在遇到项目难以解决的
问题时, 都可以在这个大家庭里寻求帮助.
公众号回复 [问题的答案] 进入: java 中 Integer 包装类的基本数据类型是?
如果你也经历过遇到项目难题, 无从下手,
他人有可能可以给你提供一些思路和看法, 一百个人就有一百种思路,
同样, 如果你也乐于帮助别人, 那解决别人遇到的问题, 也同样对你是一种锻炼.
欢迎来公众号 [侠梦的开发笔记] , 回复干货, 领取精选学习视频一份
来源: https://www.cnblogs.com/hyq0823/p/12271402.html