java并发编程实战(7) 并发工具JUC之CountDownLatch

╰半橙微兮° 2022-05-08 07:00 500阅读 0赞

一、CountDownLatch是什么?


CountDownLatch 是多线程控制JUt(java.util.concurrent.CountDownLatch)的一个工具类,它被称为 门阀 、 计数器 或者 闭锁 。这个工具经常用来用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)

CountDownLatch 能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。它相当于是一个计数器,这个计数器的初始值就是线程的数量,每当一个任务完成后,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经任务了,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。

二、CountDownLatch类说明和原理


1、大概源码

" class="reference-link">watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hndWlzdQ_size_16_color_FFFFFF_t_70

1、构造函数:

CountDownLatch初始化的时候,需要提供一个整形数字count,数字代表着线程需要调用countDown()方法的次数,当计数为0时,线程才会继续执行await()方法后的其他内容。CountDownLatch(int count)

2、getCount:

  1. 返回当前的计数count

3、public void countDown()

  1. 调用此方法后,会减少计数count的值。递减后如果为0,则会释放所有等待的线程

4、public void await() throws InterruptedException

调用CountDownLatch对象的await方法后。会让当前线程阻塞,直到计数count递减至0。

如果当前线程数大于0,则当前线程在线程调度中将变得不可用,并处于休眠状态,直到发生以下两种情况之一:

1、调用countDown()方法,将计数count递减至0。

2、当前线程被其他线程打断

5、public boolean await(long timeout, TimeUnit unit) throws InterruptedException

同时await还提供一个带参数和返回值的方法。

如果计数count正常递减,返回0后,await方法会返回true并继续执行后续逻辑。

或是,尚未递减到0,而到达了指定的时间间隔后,方法返回false。

如果时间小于等于0,则此方法不执行等待。

#

2、原理

从源码可以看出,CountDownLatch是依赖于AbstractQueuedSynchronizer来实现这一系列逻辑的。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hndWlzdQ_size_16_color_FFFFFF_t_70 1

队列同步器AbstractQueuedSynchronizer 是一个用来构建锁和同步器的框架,它在内部定义了一个被标识为volatile的名为state的变量,用来表示同步状态。

多个线程之间可以通过AQS来独占式或共享式的抢占资源。

并且它通过内置的FIFO队列来完成线程的排队工作。

CountDownLatch中的Sync会优先尝试修改state的值,来获取同步状态。例如,如果某个线程成功的将state的值从0修改为1,表示成功的获取了同步状态。这个修改的过程是通过CAS完成的,所以可以保证线程安全。

反之,如果修改state失败,则会将当前线程加入到AQS的队列中,并阻塞线程。

#

三、CountDownLatch


1、join阻塞等待线程完成

使用join保证线程一、线程二执行完之后,再执行System.out.println(“主线程”)

具体请看《线程同步机制:彻底搞懂相关方法wait、join、sleep、notify》

  1. package com.javademo.demo.jucdemo;
  2. import java.util.concurrent.CountDownLatch;
  3. public class CountDownLatchDemoJoin {
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread thread1 = new Thread(new Worker1());
  6. Thread thread2 = new Thread(new Worker2());
  7. thread1.start();
  8. thread2.start();
  9. thread1.join();
  10. thread2.join();
  11. System.out.println("主线程结束....");
  12. }
  13. private static class Worker1 implements Runnable {
  14. @Override
  15. public void run() {
  16. System.out.println("-线程1执行");
  17. try {
  18. Thread.sleep(5000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }finally {
  22. System.out.println("线程1完成--我休眠5秒");
  23. }
  24. }
  25. }
  26. private static class Worker2 implements Runnable {
  27. @Override
  28. public void run() {
  29. System.out.println("-线程2执行");
  30. try {
  31. Thread.sleep(3000);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }finally {
  35. System.out.println("线程2完成--我休眠3秒");
  36. }
  37. }
  38. }
  39. }

打印结果如下:

20210330190046885.png

可以看出2个线程是并行执行的。启动顺序,并不和执行完毕的顺序一致,但可以明确的是,主线程为一直阻塞,直到2个线程执行完毕。

使用CountDownLatch保证线程一、线程二执行完之后,再执行System.out.println(“主线程结束….“)

  1. package com.javademo.demo.jucdemo;
  2. import java.util.concurrent.CountDownLatch;
  3. public class CountDownLatchDemo {
  4. private static CountDownLatch countDownLatch = new CountDownLatch(2);
  5. public static void main(String[] args) throws InterruptedException {
  6. new Thread(new Worker1()).start();
  7. new Thread(new Worker2()).start();
  8. countDownLatch.await();
  9. System.out.println("主线程结束....");
  10. }
  11. private static class Worker1 implements Runnable {
  12. @Override
  13. public void run() {
  14. System.out.println("-线程1执行");
  15. try {
  16. Thread.sleep(5000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }finally {
  20. System.out.println("线程1完成--我休眠5秒");
  21. countDownLatch.countDown();
  22. }
  23. }
  24. }
  25. private static class Worker2 implements Runnable {
  26. @Override
  27. public void run() {
  28. System.out.println("-线程2执行");
  29. try {
  30. Thread.sleep(3000);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }finally {
  34. System.out.println("线程2完成--我休眠3秒");
  35. countDownLatch.countDown();
  36. }
  37. }
  38. }
  39. }

2、数据库连接池Druid的CountDownLatch用法

典型的应用场景就是当一个服务启动时,同时会加载很多组件和服务,这时候主线程会等待组件和服务的加载。当所有的组件和服务都加载完毕后,主线程和其他线程在一起完成某个任务。

阿里巴巴的数据库连接池Druid中也用了countDownLatch来保证初始化。
image.png

  1. // 开启创建连接的线程,如果线程池createScheduler为null,
  2. //则开启单个创建连接的线程
  3. createAndStartCreatorThread();
  4. // 开启销毁过期连接的线程
  5. createAndStartDestroyThread();

四、CountDownLatch和Join用法的区别


在使用join()中,多个线程只有在执行完毕之后才能被解除阻塞,而在CountDownLatch中,线程可以在任何时候任何位置调用countdown方法减少计数,通过这种方式,我们可以更好地控制线程的解除阻塞,而不是仅仅依赖于连接线程的完成。

join()方法的执行逻辑如下图所示:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2hndWlzdQ_size_16_color_FFFFFF_t_70 2

五、总结和注意事项


CountDownLatch(int N) 中的计数器,可以让我们支持最多等待N个线程的操作完成,或是一个线程操作N次。

如果仅仅只需要等待线程的执行完毕,那么join可能就能满足。但是如果需要灵活的控制线程,使用CountDownLatch。

注意事项:countDownLatch.countDown();

这一句话尽量写在finally中,或是保证此行代码前的逻辑正常运行,因为在一些情况下,出现异常会导致无法减1,然后出现死锁。

CountDownLatch 是一次性使用的,当计数值在构造函数中初始化后,就不能再对其设置任何值,当 CountDownLatch 使用完毕,也不能再次被使用。

发表评论

表情:
评论列表 (有 0 条评论,500人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java并发编程CountDownLatch

    一、场景描述 在多线程程序设计中,经常会遇到一个线程等待一个或多个线程的场景 例如:百米赛跑,十名运动员同时起跑,由于速度的快慢,肯定有先到达和后到达的,而终点有个统计