经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring Boot » 查看文章
面试官:实战中用过CountDownLatch吗?详细说一说,我:啊这
来源:cnblogs  作者:JavaBuild  时间:2024/4/15 9:49:12  对本文有异议

写在开头

在很多的面经中都看到过提问 CountDownLatch 的问题,正好我们最近也在梳理学习AQS(抽象队列同步器),而CountDownLatch又是其中典型的代表,我们今天就继续来学一下这个同步工具类!

CountDownLatch有何作用?

我们知道AQS是专属于构造锁和同步器的一个抽象工具类,基于它Java构造出了大量的常用同步工具,如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue等等,我们今天的主角CountDownLatch同样如此。

CountDownLatch(倒时器)允许N个线程阻塞在同一个地方,直至所有线程的任务都执行完毕。CountDownLatch 有一个计数器,可以通过countDown()方法对计数器的数目进行减一操作,也可以通过await()方法来阻塞当前线程,直到计数器的值为 0。

CountDownLatch的底层原理

想要迅速了解一个Java类的内部构造,或者使用原理,最快速直接的办法就是看它的源码,这是很多初学者比较抵触的,会觉得很多封装起来的源码都晦涩难懂,诚然很多类内部实现是复杂,但我们作为Java工程师也不能只追求CRUD呀,培养自己看源码的习惯,硬着头皮看段时间,代码能力绝对会提升的!

废话说的有点多了,我们直接进入CountDownLatch内部去看看它的底层原理吧

【源码解析1】

  1. //几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS
  2. private static final class Sync extends AbstractQueuedSynchronizer {
  3. private static final long serialVersionUID = 4982264981922014374L;
  4. Sync(int count) {
  5. setState(count);
  6. }
  7. int getCount() {
  8. return getState();
  9. }
  10. }
  11. private final Sync sync;
  12. //构造方法中初始化count值
  13. public CountDownLatch(int count) {
  14. if (count < 0) throw new IllegalArgumentException("count < 0");
  15. this.sync = new Sync(count);
  16. }

几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS,并实现其提供的钩子方法,通过封装AQS中的state为count来确定多个线程的计时器。

countDown()方法

【源码解析2】

  1. //核心方法,内部封装了共享模式下的线程释放
  2. public void countDown() {
  3. //内部类Sync,继承了AQS
  4. sync.releaseShared(1);
  5. }
  6. //AQS内部的实现
  7. public final boolean releaseShared(int arg) {
  8. if (tryReleaseShared(arg)) {
  9. //唤醒后继节点
  10. doReleaseShared();
  11. return true;
  12. }
  13. return false;
  14. }

在CountDownLatch中通过countDown来减少倒计时数,这是最重要的一个方法,我们继续跟进源码看到它通过releaseShared()方法去释放锁,这个方法是AQS内部的默认实现方法,而在这个方法中再一次的调用了tryReleaseShared(arg),这是一个AQS的钩子方法,方法内部仅有默认的异常处理,真正的实现由CountDownLatch内部类Sync完成

image

【源码解析3】

  1. // 对 state 进行递减,直到 state 变成 0;
  2. // 只有 count 递减到 0 时,countDown 才会返回 true
  3. protected boolean tryReleaseShared(int releases) {
  4. // 自选检查 state 是否为 0
  5. for (;;) {
  6. int c = getState();
  7. // 如果 state 已经是 0 了,直接返回 false
  8. if (c == 0)
  9. return false;
  10. // 对 state 进行递减
  11. int nextc = c-1;
  12. // CAS 操作更新 state 的值
  13. if (compareAndSetState(c, nextc))
  14. return nextc == 0;
  15. }
  16. }

await()方法

除了countDown()方法外,在CountDownLatch中还有一个重要方法就是 await ,在多线程环境下,线程的执行顺序并不一致,因此,对于一个倒时器也说,先开始的线程应该阻塞等待直至最后一个线程执行完成,而实现这一效果的就是await()方法!

【源码解析4】

  1. // 等待(也可以叫做加锁)
  2. public void await() throws InterruptedException {
  3. sync.acquireSharedInterruptibly(1);
  4. }
  5. // 带有超时时间的等待
  6. public boolean await(long timeout, TimeUnit unit)
  7. throws InterruptedException {
  8. return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
  9. }

其中await()方法可以配置带有时间参数的,表示最大阻塞时间,当调用 await() 的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 就会一直阻塞,也就是说 await() 之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state是否等于0,若是就会释放所有等待的线程,await() 方法之后的语句得到执行。

CountDownLatch的使用

由于await的实现步骤和countDown类似,我们就不贴源码了,大家自己跟进去也很容易看明白,我们现在直接来一个小demo感受一下如何使用CountDownLatch做一个倒时器

【代码样例1】

  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 创建一个倒计数为 3 的 CountDownLatch
  4. CountDownLatch latch = new CountDownLatch(3);
  5. Thread service1 = new Thread(new Service("3", 1000, latch));
  6. Thread service2 = new Thread(new Service("2", 2000, latch));
  7. Thread service3 = new Thread(new Service("1", 3000, latch));
  8. service1.start();
  9. service2.start();
  10. service3.start();
  11. // 等待所有服务初始化完成
  12. latch.await();
  13. System.out.println("发射");
  14. }
  15. static class Service implements Runnable {
  16. private final String name;
  17. private final int timeToStart;
  18. private final CountDownLatch latch;
  19. public Service(String name, int timeToStart, CountDownLatch latch) {
  20. this.name = name;
  21. this.timeToStart = timeToStart;
  22. this.latch = latch;
  23. }
  24. @Override
  25. public void run() {
  26. try {
  27. Thread.sleep(timeToStart);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. System.out.println(name);
  32. // 减少倒计数
  33. latch.countDown();
  34. }
  35. }
  36. }

输出:

  1. 3
  2. 2
  3. 1
  4. 发射

执行结果体现出了倒计时的效果每隔1秒进行3,2,1的倒数;其实除了倒计时器外CountDownLatch还有另外一个使用场景:实现多个线程开始执行任务的最大并行性

多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。

具体做法是: 初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

【代码样例2】

  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. CountDownLatch countDownLatch = new CountDownLatch(1);
  4. for (int i = 0; i < 5; i++) {
  5. new Thread(() -> {
  6. try {
  7. System.out.println("5位运动员就位!");
  8. //等待发令枪响
  9. countDownLatch.await();
  10. System.out.println(Thread.currentThread().getName() + "起跑!");
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }).start();
  15. }
  16. // 裁判准备发令
  17. Thread.sleep(2000);
  18. //发令枪响
  19. countDownLatch.countDown();
  20. }
  21. }

输出:

  1. 5位运动员就位!
  2. 5位运动员就位!
  3. 5位运动员就位!
  4. 5位运动员就位!
  5. 5位运动员就位!
  6. Thread-0起跑!
  7. Thread-3起跑!
  8. Thread-4起跑!
  9. Thread-1起跑!
  10. Thread-2起跑!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

image

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

image

原文链接:https://www.cnblogs.com/JavaBuild/p/18134174

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号