1. 前言
在很多的面经中都看到过提问 CountDownLatch 的问题,正好我们最近也在梳理学习AQS(抽象队列同步器)、CAS操作等知识,而 CountDownLatch 又是JUC包下一个比较常见的同步工具类,我们今天就继续来学一下这个同步工具类!
2. CountDownLatch有什么用
我们知道AQS是专属于构造锁和同步器的一个抽象工具类,基于它Java构造出了大量的常用同步工具,如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue等等,我们今天的主角CountDownLatch同样如此。
CountDownLatch(倒时器)允许N个线程阻塞在同一个地方,直至所有线程的任务都执行完毕。CountDownLatch 有一个计数器,可以通过countDown()方法对计数器的数目进行减一操作,也可以通过await()方法来阻塞当前线程,直到计数器的值为 0。
这个就很类似我们的Moba类游戏的游戏加载过程,所有玩家几乎是一起进入游戏,加载快的玩家要等待加载慢的玩家,只有当全部玩家加载完成才能进入游戏,而CountDownLatch就类似于这个过程中的发令枪。
3. CountDownLatch底层原理
想要迅速了解一个Java类的内部构造,或者使用原理,最快速直接的办法就是看它的源码,这是很多初学者比较抵触的,会觉得很多封装起来的源码都晦涩难懂,诚然很多类内部实现是复杂,我也是慢慢从刚开始阅读Mybatis源码,到后来阅读JDK多线程相关的源码,尝试培养自己看源码的习惯,硬着头皮看段时间还是有不少收获的。
好的,我们直接进入CountDownLatch内部去看看它的底层原理吧
//几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } } private final Sync sync; //构造方法中初始化count值 public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
是不是很熟悉?对又是Sync,与 Semaphore信号量一样,几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS
3.1. countDown()方法
//核心方法,内部封装了共享模式下的线程释放 public void countDown() { //内部类Sync,继承了AQS sync.releaseShared(1); } //AQS内部的实现 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { //唤醒后继节点 doReleaseShared(); return true; } return false; }
在CountDownLatch中通过countDown来减少倒计时数,这是最重要的一个方法,我们继续跟进源码看到它通过releaseShared()方法去释放锁,这个方法是AQS内部的默认实现方法,而在这个方法中再一次的调用了tryReleaseShared(arg),这是一个AQS的钩子方法,方法内部仅有默认的异常处理,真正的实现由CountDownLatch内部类Sync完成,如下
// 对 state 进行递减,直到 state 变成 0; // 只有 count 递减到 0 时,countDown 才会返回 true protected boolean tryReleaseShared(int releases) { // 自选检查 state 是否为 0 for (;;) { int c = getState(); // 如果 state 已经是 0 了,直接返回 false if (c == 0) return false; // 对 state 进行递减 int nextc = c-1; // CAS 操作更新 state 的值 if (compareAndSetState(c, nextc)) return nextc == 0; } }
当这个tryReleaseShared函数返回true时,也就是state扣减到了零,就会调用doReleaseShared唤醒CLH队列中阻塞等待的线程
3.2. await()方法
除了countDown()方法外,在CountDownLatch中还有一个重要方法就是await,在多线程环境下,线程的执行顺序并不一致,因此,对于一个倒时器也说,先开始的线程应该阻塞等待直至最后一个线程执行完成,而实现这一效果的就是await()方法!
// 等待 public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // 带有超时时间的等待 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); }
其中await()方法可以配置带有时间参数的,表示最大阻塞时间,当调用 await() 的时候,我们会调用aqs的一个模板方法acquireSharedInterruptibly(arg),如下:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
Thread.interrupted()为判断线程书否为中断状态,如果为中断状态,抛出中断异常,否则会调用tryAcquireShared(arg)方法,tryAcquireShared方法为AQS的钩子函数,由静态内部类Snyc实现,如下
1. proprotected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
也就是说,当前state = 0就返回1,也就不会执行doAcquireSharedInterruptibly(arg),直接放行没有堵塞。否则会执行doAcquireSharedInterruptibly(arg)这个方法,这个方法内部主要是将当前线程加入CLH队列阻塞等待。
4. CountDownLatch的基本使用
由于await的实现步骤和countDown类似,我们就不贴源码了,大家自己跟进去也很容易看明白,我们现在直接来一个小demo感受一下如何使用CountDownLatch做一个倒时器
public class Test { public static void main(String[] args) throws InterruptedException { // 创建一个倒计数为 3 的 CountDownLatch CountDownLatch latch = new CountDownLatch(3); Thread service1 = new Thread(new Service("3", 1000, latch)); Thread service2 = new Thread(new Service("2", 2000, latch)); Thread service3 = new Thread(new Service("1", 3000, latch)); service1.start(); service2.start(); service3.start(); // 等待所有服务初始化完成 latch.await(); System.out.println("发射"); } static class Service implements Runnable { private final String name; private final int timeToStart; private final CountDownLatch latch; public Service(String name, int timeToStart, CountDownLatch latch) { this.name = name; this.timeToStart = timeToStart; this.latch = latch; } @Override public void run() { try { Thread.sleep(timeToStart); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name); // 减少倒计数 latch.countDown(); } } }
输出结果
3 2 1 发射
执行结果体现出了倒计时的效果每隔1秒进行3,2,1的倒数;其实除了倒计时器外CountDownLatch还有另外一个使用场景:实现多个线程开始执行任务的最大并行性
多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。
具体做法是: 初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
public class Test { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); for (int i = 0; i < 5; i++) { new Thread(() -> { try { System.out.println("5位运动员就位!"); //等待发令枪响 countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "起跑!"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } // 裁判准备发令 Thread.sleep(2000); //发令枪响 countDownLatch.countDown(); } }
输出结果
5位运动员就位! 5位运动员就位! 5位运动员就位! 5位运动员就位! 5位运动员就位! Thread-0起跑! Thread-3起跑! Thread-4起跑! Thread-1起跑! Thread-2起跑!
5. 总结
CountDownLatch 是一个多线程同步辅助类,它允许一个或多个线程等待一系列操作在其他线程中完成。这个机制类似于一场赛跑,选手们在起跑线准备,等待发令枪响后才能开始比赛。在 CountDownLatch 的场景中,线程们等待一个共同的信号,只有当计数器降至零时,它们才能继续执行。
CountDownLatch 提供了两个主要方法:countDown() 和 await()。countDown()方法用于将计数器减一,而 await() 方法会阻塞调用线程,直到计数器达到零。这种机制确保了所有线程都会等待必要的操作完成。
内部实现上,CountDownLatch 通过一个静态内部类 Sync 继承自 AbstractQueuedSynchronizer(AQS)。AQS 提供了一个框架,用于构建自定义的同步器。在 CountDownLatch 中,Sync 类通过重写 AQS 的钩子方法 tryReleaseShared() 和 tryAcquireShared() 来实现其同步机制。
tryReleaseShared() 方法用于在共享模式下尝试释放资源
tryAcquireShared() 方法用于在共享模式下尝试获取资源
我们可以发现,几乎所有基于AQS构造的同步类,实现原理都是差不多的,都是通过维护AQS中被volatile修饰的state变量作为竞态条件来实现线程同步。