AQS源码探究_07 CountDownLatch源码分析

Bertha 。 2023-01-19 13:49 287阅读 0赞
  • 在学习CountDownLatch之前,最好仔细理解下前面AQS相关的几篇文章,配合着自己搭建的源码环境进行逐行跟踪,防止迷路~

1、CountDownLatch简介

CountDownLatch,是一个简单的同步器,它的含义是允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作

CountDownLatch的通常用法和Thread.join()有点类似,等待其它线程都完成后再执行主任务。

2、入门案例分析

案例1:

  • 对于像我一样的学生来说,CountDwonLatch的实际开发应用很少,甚至有同学没有接触过它。但是在并发条件下,这个类的使用还是很常见的,所以先引入2个案例去了解下它的用途:
  • 借助CountDownLatch,控制主线程等待子线程完成再执行

    /* date: 2021/5/7 10:01 @author csp /
    public class CountDownLatchTest01 {

    1. private static final int TASK_COUNT = 8;
    2. private static final int THREAD_CORE_SIZE = 10;
    3. public static void main(String[] args) throws InterruptedException {
    4. // 实例化CountDownLatch,指定初始计数值为TASK_COUNT(8)
    5. CountDownLatch latch = new CountDownLatch(TASK_COUNT);
    6. // 通过Executors创建一个初始容量为THREAD_CORE_SIZE(10)的线程池
    7. // (注意:在阿里巴巴开发手册中,建议不要使用Executors直接去创建线程池,
    8. // 要使用其内部调用的ThreadPoolExecutor去手动设置线程池的相关参数,并创建线程池)
    9. Executor executor = Executors.newFixedThreadPool(THREAD_CORE_SIZE);
    10. // 依次向线程池中投入8个执行的线程
    11. for(int i = 0; i < 8; i++) {
    12. // i -> taskId 任务id
    13. // latch -> 同步计数器的值
    14. executor.execute(new WorkerRunnable(i, latch));
    15. }
    16. System.out.println("主线程等待所有子任务完成....");
    17. long mainWaitStartTimeMillis = System.currentTimeMillis();
    18. latch.await();
    19. long mainWaitEndTimeMillis = System.currentTimeMillis();
    20. System.out.println("主线程等待时长:" + (mainWaitEndTimeMillis - mainWaitStartTimeMillis));
    21. }
    22. /** * 工作线程 */
    23. static class WorkerRunnable implements Runnable {
    24. /** * 任务id */
    25. private int taskId;
    26. /** * CountDownLatch同步计数器 */
    27. private CountDownLatch latch;
    28. @Override
    29. public void run() {
    30. doWorker();
    31. }
    32. /** * 工作方法 */
    33. public void doWorker() {
    34. System.out.println("任务ID:" + taskId + ",正在执行任务中....");
    35. try {
    36. // 休眠5s,模拟正在处理任务
    37. TimeUnit.SECONDS.sleep(5);
    38. } catch (InterruptedException e) {
    39. } finally {
    40. // latch = latch-1 :
    41. // 计数器的值latch开始是TASK_COUNT,每执行完一个doWorker方法就-1
    42. // 直到latch值减小为0,才能继续执行latch.await();之后的方法
    43. latch.countDown();
    44. }
    45. System.out.println("任务ID:" + taskId + ",任务执行结束!");
    46. }
    47. public WorkerRunnable(int taskId, CountDownLatch latch) {
    48. this.taskId = taskId;
    49. this.latch = latch;
    50. }
    51. }

    }

运行结果如下:

  1. 主线程等待所有子任务完成....
  2. 任务ID0,正在执行任务中....
  3. 任务ID1,正在执行任务中....
  4. 任务ID2,正在执行任务中....
  5. 任务ID4,正在执行任务中....
  6. 任务ID5,正在执行任务中....
  7. 任务ID3,正在执行任务中....
  8. 任务ID6,正在执行任务中....
  9. 任务ID7,正在执行任务中....
  10. 任务ID0,任务执行结束!
  11. 任务ID5,任务执行结束!
  12. 任务ID3,任务执行结束!
  13. 任务ID4,任务执行结束!
  14. 任务ID2,任务执行结束!
  15. 任务ID1,任务执行结束!
  16. 任务ID7,任务执行结束!
  17. 任务ID6,任务执行结束!
  18. 主线程等待时长:5000

案例2:

  • 执行任务的线程,也可能是多对多的关系:本案例就来了解一下,借助CountDownLatch,使主线程控制子线程同时开启,主线程再去阻塞等待子线程结束!

    /* date: 2021/5/7 10:01 @author csp /
    public class CountDownLatchTest02 {

    1. // 主线程
    2. public static void main(String[] args) throws InterruptedException {
    3. // 开始信号:CountDownLatch初始值为1
    4. CountDownLatch startSignal = new CountDownLatch(1);
    5. // 结束信号:CountDownLatch初始值为10
    6. CountDownLatch doneSignal = new CountDownLatch(10);
    7. // 开启10个线程,
    8. for(int i = 0; i < 10; i++) {
    9. new Thread(new Worker(i, startSignal, doneSignal)).start();
    10. }
    11. // 这里让主线程休眠500毫秒,确保所有子线程已经启动,并且阻塞在startSignal栅栏处
    12. TimeUnit.MILLISECONDS.sleep(500);
    13. // 因为startSignal 栅栏值为1,所以主线程只要调用一次countDown()方法
    14. // 那么所有调用startSignal.await()阻塞的子线程,就都可以通过栅栏了
    15. System.out.println("子任务栅栏已开启...");
    16. startSignal.countDown();
  1. System.out.println("等待子任务结束...");
  2. long startTime = System.currentTimeMillis();
  3. // 等待所有子任务结束,主线程再继续往下执行
  4. doneSignal.await();
  5. long endTime = System.currentTimeMillis();
  6. System.out.println("所有子任务已经运行结束,耗时:" + (endTime - startTime));
  7. }
  8. /** * 工作线程:子线程 */
  9. static class Worker implements Runnable {
  10. /** * 开启信号 */
  11. private final CountDownLatch startSignal;
  12. /** * 结束信号 */
  13. private final CountDownLatch doneSignal;
  14. /** * 任务id */
  15. private int id;
  16. @Override
  17. public void run() {
  18. try {
  19. // 为了让所有线程同时开始任务,我们让所有线程先阻塞在这里(相当于一个栅栏)
  20. // 等到startSignal值被countDown为0时才往下继续执行:等大家都准备好了,再打开这个门栓
  21. startSignal.await();
  22. System.out.println("子任务-" + id + ",开启时间:" + System.currentTimeMillis());
  23. // sleep 5秒,模拟线程处理任务
  24. doWork();
  25. } catch (InterruptedException e) {
  26. }finally {
  27. doneSignal.countDown();
  28. }
  29. }
  30. private void doWork() throws InterruptedException {
  31. TimeUnit.SECONDS.sleep(5);
  32. }
  33. public Worker(int id, CountDownLatch startSignal, CountDownLatch doneSignal) {
  34. this.id = id;
  35. this.startSignal = startSignal;
  36. this.doneSignal = doneSignal;
  37. }
  38. }
  39. }

执行结果:

  1. 子任务栅栏已开启...
  2. 等待子任务结束...
  3. 子任务-9,开启时间:1620432037554
  4. 子任务-8,开启时间:1620432037554
  5. 子任务-3,开启时间:1620432037554
  6. 子任务-4,开启时间:1620432037554
  7. 子任务-1,开启时间:1620432037554
  8. 子任务-0,开启时间:1620432037554
  9. 子任务-5,开启时间:1620432037554
  10. 子任务-7,开启时间:1620432037554
  11. 子任务-2,开启时间:1620432037554
  12. 子任务-6,开启时间:1620432037554
  13. 所有子任务已经运行结束,耗时:5002

上面代码中startSignal.await();就相当于一个栅栏,把所有子线程都抵挡在他们的run方法,等待主线程执行startSignal.countDown();,即关闭栅栏之后,所有子线程在同时继续执行他们自己的run方法,如下图:

请添加图片描述

案例3:

  1. /** * date: 2021/5/8 * * @author csp */
  2. public class CountDownLatchTest03 {
  3. public static void main(String[] args) {
  4. // 声明CountDownLatch计数器,初始值为2
  5. CountDownLatch latch = new CountDownLatch(2);
  6. // 任务线程1:
  7. Thread t1 = new Thread(() -> {
  8. try {
  9. Thread.sleep(5000);
  10. } catch (InterruptedException ignore) {
  11. }
  12. // 休息 5 秒后(模拟线程工作了 5 秒),调用 countDown()
  13. latch.countDown();
  14. }, "t1");
  15. // 任务线程2:
  16. Thread t2 = new Thread(() -> {
  17. try {
  18. Thread.sleep(10000);
  19. } catch (InterruptedException ignore) {
  20. }
  21. // 休息 10 秒后(模拟线程工作了 10 秒),调用 countDown()
  22. latch.countDown();
  23. }, "t2");
  24. // 线程1、2开始执行
  25. t1.start();
  26. t2.start();
  27. // 任务线程3:
  28. Thread t3 = new Thread(() -> {
  29. try {
  30. // 阻塞,等待 state 减为 0
  31. latch.await();
  32. System.out.println("线程 t3 从 await 中返回了");
  33. } catch (InterruptedException e) {
  34. System.out.println("线程 t3 await 被中断");
  35. Thread.currentThread().interrupt();
  36. }
  37. }, "t3");
  38. // 任务线程4:
  39. Thread t4 = new Thread(() -> {
  40. try {
  41. // 阻塞,等待 state 减为 0
  42. latch.await();
  43. System.out.println("线程 t4 从 await 中返回了");
  44. } catch (InterruptedException e) {
  45. System.out.println("线程 t4 await 被中断");
  46. Thread.currentThread().interrupt();
  47. }
  48. }, "t4");
  49. // 线程3、4开始执行
  50. t3.start();
  51. t4.start();
  52. }
  53. }

执行结果:

  1. 线程 t4 await 中返回了
  2. 线程 t3 await 中返回了

3、源码分析

Sync内部类

  • CountDownLatch的Sync内部类继承AQS

    private static final class Sync extends AbstractQueuedSynchronizer {

    1. private static final long serialVersionUID = 4982264981922014374L;
    2. // 传入初始count次数
    3. Sync(int count) {
    4. setState(count);
    5. }
    6. // 获取还剩的count次数
    7. int getCount() {
    8. return getState();
    9. }
    10. // 尝试获取共享锁
    11. protected int tryAcquireShared(int acquires) {
    12. // 注意,这里state等于0的时候返回的是1,也就是说count减为0的时候获取锁总是成功
    13. // state不等于0的时候返回的是-1,也就是count不为0的时候总是要排队
    14. return (getState() == 0) ? 1 : -1;
    15. }
    16. // 尝试释放锁:
    17. // 更新 AQS.state 值,每调用一次,state值减一,当state -1 正好为0时,返回true
    18. protected boolean tryReleaseShared(int releases) {
    19. for (;;) {
    20. // 获取当前state的值(AQS.state)
    21. int c = getState();
    22. // 如果state等于0了,说明已释放锁,无法再释放了,这里返回false
    23. if (c == 0)
    24. return false;
    25. //执行到这里,说明 state > 0
    26. // 如果count>0,则将count的值减1
    27. int nextc = c-1;
    28. // 原子更新state的值:
    29. // cas成功,说明当前线程执行 tryReleaseShared 方法 c-1之前,没有其它线程 修改过 state。
    30. if (compareAndSetState(c, nextc))
    31. // 减为0的时候返回true,这时会唤醒后面排队的线程
    32. // 说明当前调用 countDown() 方法的线程就是需要触发 唤醒操作的线程!
    33. return nextc == 0;
    34. }
    35. }

    }

Sync重写了tryAcquireShared()tryReleaseShared()方法,并把count存到state变量中去。这里要注意一下,上面两个方法的参数并没有被用到。

构造方法

  1. // 构造方法需要传入一个count,也就是初始次数。
  2. public CountDownLatch(int count) {
  3. if (count < 0) throw new IllegalArgumentException("count < 0");
  4. // 初始化Sync内部类:
  5. this.sync = new Sync(count);
  6. }

await()方法

await()方法是等待其它线程完成的方法,它会先尝试获取一下共享锁,如果失败则进入AQS的队列中排队等待被唤醒。

根据上面Sync的源码,我们知道,state不等于0的时候tryAcquireShared()返回的是-1,也就是说count未减到0的时候,所有调用await()方法的线程都要排队。

  1. public void await() throws InterruptedException {
  2. // 调用AQS的acquireSharedInterruptibly()方法:
  3. sync.acquireSharedInterruptibly(1);
  4. }

AQS中的acquireInterruptibly方法:

  1. // 位于AQS中:可以响应中断获取共享锁的方法
  2. public final void acquireSharedInterruptibly(int arg)
  3. throws InterruptedException {
  4. // CASE1: Thread.interrupted()
  5. // 条件成立:说明当前调用await方法的线程已经是中断状态,直接抛出异常即可~
  6. if (Thread.interrupted())
  7. throw new InterruptedException();
  8. // CASE2: tryAcquireShared(arg) < 0 注意:-1表示获取到了共享锁,1表示没有获取共享锁
  9. // 条件成立:说明当前AQS的state是大于0的,此时将线程入队,然后等待被唤醒
  10. // 条件不成立:说明AQS的state = 0,此时就不会阻塞线程:
  11. // 即,对应业务层面来说,执行任务的线程这时已经将latch打破了。然后其他再调用latch.await的线程,就不会在这里阻塞了
  12. if (tryAcquireShared(arg) < 0)
  13. // 采用共享中断模式
  14. doAcquireSharedInterruptibly(arg);
  15. }
  16. // 位于AQS中:采用共享中断模式
  17. private void doAcquireSharedInterruptibly(int arg)
  18. throws InterruptedException {
  19. // 将调用latch.await()方法的线程包装成node加入到AQS的阻塞队列当中
  20. final Node node = addWaiter(Node.SHARED);
  21. boolean failed = true;
  22. try {
  23. for (;;) {
  24. // 获取当前线程节点的前驱节点
  25. final Node p = node.predecessor();
  26. // 条件成立,说明当前线程对应的节点为head.next节点
  27. if (p == head) {
  28. // head.next节点就有权利获取共享锁了..
  29. int r = tryAcquireShared(arg);
  30. if (r >= 0) {
  31. setHeadAndPropagate(node, r);
  32. p.next = null; // help GC
  33. failed = false;
  34. return;
  35. }
  36. }
  37. // shouldParkAfterFailedAcquire 会给当前线程找一个好爸爸,最终给爸爸节点设置状态为 signal(-1),返回true
  38. // parkAndCheckInterrupt 挂起当前节点对应的线程...
  39. if (shouldParkAfterFailedAcquire(p, node) &&
  40. parkAndCheckInterrupt())
  41. throw new InterruptedException();
  42. }
  43. } finally {
  44. if (failed)
  45. cancelAcquire(node);
  46. }
  47. }

图解分析:

请添加图片描述

countDown()方法

countDown()方法,会释放一个共享锁,也就是count的次数会减1。

根据上面Sync的源码,我们知道,tryReleaseShared()每次会把count的次数减1,当其减为0的时候返回true,这时候才会唤醒等待的线程。

注意,doReleaseShared()是唤醒等待的线程,这个方法我们在前面的章节中分析过了。

  1. public void countDown() {
  2. // 释放共享锁
  3. sync.releaseShared(1);
  4. }
  5. // java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseShared()
  6. public final boolean releaseShared(int arg) {
  7. // tryReleaseShared(arg) 尝试释放共享锁,如果成功了,就唤醒排队的线程:
  8. // 条件成立:说明当前调用latch.countDown() 方法线程正好是 state - 1 == 0 的这个线程,需要做触发唤醒await状态的线程。
  9. if (tryReleaseShared(arg)) { // Sync的内部成员方法
  10. // 唤醒等待的线程:
  11. // 调用countDown()方法的线程 只有一个线程会进入到这个 if块 里面,去调用 doReleaseShared() 唤醒 阻塞状态的线程的逻辑。
  12. doReleaseShared();
  13. return true;
  14. }
  15. return false;
  16. }
  17. /** * 都有哪几种路径会调用到doReleaseShared方法呢? * 1.latch.countDown() -> AQS.state == 0 -> doReleaseShared() 唤醒当前阻塞队列内的 head.next 对应的线程。 * 2.被唤醒的线程 -> doAcquireSharedInterruptibly parkAndCheckInterrupt() 唤醒 -> setHeadAndPropagate() -> doReleaseShared() */
  18. // AQS.doReleaseShared
  19. private void doReleaseShared() {
  20. for (;;) {
  21. // 获取当前AQS 内的 头结点
  22. Node h = head;
  23. // 条件一:h != null 成立,说明阻塞队列不为空..
  24. // 不成立:h == null 什么时候会是这样呢?
  25. // latch创建出来后,没有任何线程调用过 await() 方法之前,有线程调用latch.countDown()操作 且触发了 唤醒阻塞节点的逻辑..
  26. // 条件二:h != tail 成立,说明当前阻塞队列内,除了head节点以外 还有其他节点。
  27. // h == tail -> head 和 tail 指向的是同一个node对象。 什么时候会有这种情况呢?
  28. // 1. 正常唤醒情况下,依次获取到 共享锁,当前线程执行到这里时 (这个线程就是 tail 节点。)
  29. // 2. 第一个调用await()方法的线程 与 调用countDown()且触发唤醒阻塞节点的线程 出现并发了..
  30. // 因为await()线程是第一个调用 latch.await()的线程,此时队列内什么也没有,它需要补充创建一个Head节点,然后再次自旋时入队
  31. // 在await()线程入队完成之前,假设当前队列内 只有 刚刚补充创建的空元素 head 。
  32. // 同期,外部有一个调用countDown()的线程,将state 值从1,修改为0了,那么这个线程需要做 唤醒 阻塞队列内元素的逻辑..
  33. // 注意:调用await()的线程 因为完全入队完成之后,再次回到上层方法 doAcquireSharedInterruptibly 会进入到自旋中,
  34. // 获取当前元素的前驱,判断自己是head.next, 所以接下来该线程又会将自己设置为 head,然后该线程就从await()方法返回了...
  35. if (h != null && h != tail) {
  36. // 执行到if里面,说明当前head 一定有 后继节点!
  37. int ws = h.waitStatus;
  38. // 当前head状态 为 signal 说明 后继节点并没有被唤醒过呢...
  39. if (ws == Node.SIGNAL) {
  40. // 唤醒后继节点前 将head节点的状态改为 0
  41. // 这里为什么,使用CAS呢? 回头说...
  42. // 当doReleaseShared方法 存在多个线程 唤醒 head.next 逻辑时,
  43. // CAS 可能会失败...
  44. // 案例:
  45. // t3 线程在if(h == head) 返回false时,t3 会继续自旋. 参与到 唤醒下一个head.next的逻辑..
  46. // t3 此时执行到 CAS WaitStatus(h,Node.SIGNAL, 0) 成功.. t4 在t3修改成功之前,也进入到 if (ws == Node.SIGNAL) 里面了
  47. // 但是t4 修改 CAS WaitStatus(h,Node.SIGNAL, 0) 会失败,因为 t3 改过了...
  48. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  49. continue; // loop to recheck cases
  50. // 唤醒后继节点
  51. unparkSuccessor(h);
  52. }
  53. else if (ws == 0 &&
  54. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  55. continue; // loop on failed CAS
  56. }
  57. // 条件成立:
  58. // 1.说明刚刚唤醒的 后继节点,还没执行到 setHeadAndPropagate方法里面的 设置当前唤醒节点为head的逻辑。
  59. // 这个时候,当前线程 直接跳出去...结束了..
  60. // 此时用不用担心,唤醒逻辑 在这里断掉呢?、
  61. // 不需要担心,因为被唤醒的线程 早晚会执行到doReleaseShared方法。
  62. // 2.h == null latch创建出来后,没有任何线程调用过 await() 方法之前,
  63. // 有线程调用latch.countDown()操作 且触发了 唤醒阻塞节点的逻辑..
  64. // 3.h == tail -> head 和 tail 指向的是同一个node对象
  65. // 条件不成立:
  66. // 被唤醒的节点 非常积极,直接将自己设置为了新的head,此时 唤醒它的节点(前驱),执行h == head 条件会不成立..
  67. // 此时 head节点的前驱,不会跳出 doReleaseShared 方法,会继续唤醒 新head 节点的后继...
  68. if (h == head) // loop if head changed
  69. break;
  70. }
  71. }

CountDownLatch.countDown()执行流程图:

请添加图片描述

总结

  • CountDownLatch表示允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作;
  • CountDownLatch使用AQS的共享锁机制实现;
  • CountDownLatch初始化的时候需要传入次数count;
  • 每次调用countDown()方法count的次数减1;
  • 每次调用await()方法的时候会尝试获取锁,这里的获取锁其实是检查AQS的state变量的值是否为0;
  • 当count的值(也就是state的值)减为0的时候会唤醒排队着的线程(这些线程调用await()进入队列);

  • 文章参考:小刘讲源码、彤哥读源码!这里吹一波刘哥讲的源码付费课,真的是一行一行的解析(TQL),不愧是架构师老油条!

发表评论

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

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

相关阅读

    相关 分析:AQS

    在开始这篇源码之前,最好先看下转载整理的[这篇文章][Link 1],有很多值得学习的地方。AQS是用来构建锁或者其他同步组件的基础框架。总体来说,它使用一个 int 成员变量

    相关 并发-AQS分析

    微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路 一、概述   谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈