JUC CountDownLatch 源码详解

基础用法

  • 初始化一个 计数器(count)。
  • 线程调用 await()阻塞,直到计数器变成 0。同时还可以支持带超时时间的await()。
  • 其他线程可以调用 countDown() 让计数器减 1。
  • 当计数器变成 0 时,所有因 await() 阻塞的线程会被同时唤醒。

流程如下:

scss 复制代码
public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        new Thread(() -> {
            try {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName() + "执行");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                countDownLatch.countDown();
            }
        }, "t1").start();
        new Thread(() -> {
            try {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName() + "执行");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                countDownLatch.countDown();
            }
        }, "t2").start();
        new Thread(() -> {
            try {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName() + "执行");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                countDownLatch.countDown();
            }
        }, "t3").start();
        Thread.currentThread().setName("main");
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + "执行");
    }

详细说明:

  • 首先new了一个CountDownLatch,计数器初始化为3
  • 接下来新建三个线程执行任务,每个任务各自结束时执行countDown。此时主主线程调用await(),处于阻塞状态
  • 三个线程任务执行完毕,count == 0,主线程停止阻塞执行任务

源码详解

属性解释

想要了解CountDownLatch,首先要了解AQS是什么。接下来对其进行简要说明。

AQS(AbstractQeueudSynchronizer)抽象队列同步器,是CLH锁的改进。是由Node组成的双向队列。

可以看到,Node内部属性主要由等待线程、前驱节点,后继节点,以及status构成。前驱节点与后继节点无需过多说明,主要来关注status代表什么,有什么作用。

status有三种状态:

WAITTING:为1。节点已经被唤醒,正在主队列中等待获取锁

COND:为2。表示节点正在条件队列中等待,等待某个条件成立

CANCELLED:为负数。表示取消,遍历时快速跳过

可以看到,CountDownLatch的同步控制就是通过Sync类实现的,Sync又继承了AQS。

初始化

arduino 复制代码
private final Sync sync;
public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
scss 复制代码
    Sync(int count) {
            setState(count);
        }

初始化CountDownLatch时需要传入计数,然后借此计数new一个Sync出来。Sync的构造函数表明就是将AQS队列的状态设为count。

await()

csharp 复制代码
public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

先调用了AQS的acquireSharedInterruptibly方法,这是AQS提供的共享模式并且可中断的获取资源的方法。可以冲进去看一眼。

java 复制代码
protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;//判断计数器代表的state是否为0,即要等待执行完的线程已经全部执行完
        }
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
        if (Thread.interrupted() ||
            (tryAcquireShared(arg) < 0 &&
             acquire(null, arg, true, true, false, 0L) < 0))
            throw new InterruptedException();
    }

可以看到,他会进行条件判断,如果线程的状态是可以打断的,则直接抛异常。

tryAcquireShared则表示判断查询锁的状态是否允许在共享模式下获取它(计数器为0时 ),如果不能获取,则调用acquire来获取锁资源或者入队。如果acquire方法也无法获取资源或者入队,则抛异常

acquire方法简单解释:

  1. 判断当前节点是否为头节点
  2. 如果是头节点或者还没有入队,则直接尝试获取锁资源
  3. 获取锁资源失败,将直接入队,阻塞线程直到被唤醒或中断

经过tryAcquireShared和acquire两轮成功判断,调用latch.await()的线程要么直接获取到了锁资源(tryAcquireShared -> state = 0),要么进入了阻塞队列(tryAcquireShared -> state > 0 && acquire != 0 )。

countDown()

调用的是AQS的releaseShared(int arg)方法,如果成功释放锁资源,则唤醒下一个节点。

arduino 复制代码
protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;//计数器中代表的线程 已经全部执行,再释放就返回false
                int nextc = c - 1;//计数器 -1
                if (compareAndSetState(c, nextc))
                    return nextc == 0;//通过CAS,判断当前线程是否为最后一个释放的线程
            }
        }
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {//如果是最后一个释放的线程,则直接唤醒调用await()的资源
            signalNext(head);
            return true;
        }
        return false;
    }

从源码可以看到,每有一个线程调用countDown,就会通过CAS将计数器-1直至0。

当减到0的时候,会进行判断并唤醒阻塞队列中的线程。

因为只有一个线程调用了await()方法,所以唤醒的线程必定是此线程。然后调用latch.await()的线程恢复正常执行。

总结流程

我将用上文所举例的代码总结流程。

这是执行结果。

首先初始化计数器为3,即status = 3。新建三个线程分别执行各自的方法,执行完毕时调用countDown()。

主线程进入await()方法,如果打断标记为true,则直接抛异常。然后调用tryAcquireShared()发现status如果为零,则直接执行主线程的方法。如果不为0,因此调用acquire尝试入队。如果入队失败,直接抛异常。否则当前线程入队成功。

新建的三个线程分别异步执行。当t2执行结束后判断status == 0,然后就调用signal方法将阻塞队列中唯一的main线程唤醒,从而调用main方法。

这就是CountDownLatch的全部执行流程。

相关推荐
七夜zippoe3 小时前
微服务配置中心高可用设计:从踩坑到落地的实战指南(一)
java·数据库·微服务
天天摸鱼的java工程师3 小时前
Java 设计模式(观察者模式)+ Redis:游戏成就系统(条件达成检测、奖励自动发放)
java·后端
忘了ʷºᵇₐ3 小时前
在hadoop中Job提交的流程
java·hadoop
编啊编程啊程3 小时前
Netty从0到1系列之RPC通信
java·spring boot·rpc·kafka·dubbo·nio
召摇3 小时前
Java Web开发从零开始:初学者完整学习指南
java·后端·面试
程序猿不脱发23 小时前
Redis 内存淘汰策略 LRU 和传统 LRU 差异
java·后端·spring
王大锤43913 小时前
2种方式从springbean中获取bean实例
java·spring boot
mumu1307梦3 小时前
SpringAI 实战:解决 Netty 超时问题,优化 OpenAiApi 配置
java·spring boot·netty·超时·timeout·openapi·springai
咖啡Beans4 小时前
了解Mybatis拦截器
java·spring boot·mybatis