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的全部执行流程。

相关推荐
Seven979 分钟前
线性数据结构
java
带刺的坐椅12 分钟前
Solon 不依赖 Java EE 是其最有价值的设计!
java·spring·web·solon·javaee
青云交15 分钟前
Java 大视界 -- 基于 Java 的大数据分布式存储在数字媒体内容存储与版权保护中的应用
java·性能优化·区块链·分布式存储·版权保护·数字媒体·ai 识别
踢球的打工仔32 分钟前
PHP面向对象(5)
android·java·php
Rover.x34 分钟前
错误:找不到或无法加载主类 @C:\Users\AppData\Local\Temp\idea_arg_file223456232
java·ide·intellij-idea
4***172736 分钟前
使用 java -jar 命令启动 Spring Boot 应用时,指定特定的配置文件的几种实现方式
java·spring boot·jar
CoderYanger1 小时前
优选算法-字符串:63.二进制求和
java·开发语言·算法·leetcode·职场和发展·1024程序员节
3***31211 小时前
java进阶1——JVM
java·开发语言·jvm
FeiHuo565151 小时前
微信个人号开发中如何高效实现API二次开发
java·开发语言·python·微信
源码技术栈1 小时前
什么是云门诊系统、云诊所系统?
java·vue.js·spring boot·源码·门诊·云门诊