一、类图
从类图中可以看到CountDownLatch有一个内部类Sync,那么可以肯定CountDownLatch是基于AQS来实现的
以下为CountDownLatch的构造函数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
二、await
CountDownLatch#await方法代码如下:
public void java.util.concurrent.CountDownLatch#await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
|
V
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//获取共享锁
if (tryAcquireShared(arg) < 0)
//自旋挂起,这段代码在第6节分析读写锁的时候进行了深入的分析,此处不再重入赘述
//大致步骤讲下:首先将线程包装成Node添加到同步队列中,然后会判断前驱节点是否为头节点,如果是头节点那么会再次进行一次获取锁的动作,因为在设置前驱节点
//为SIGNAL之前有可能有线程释放了锁,然后在判断前驱节点是否为SIGNAL,如果不是将前驱节点设置为SIGNAL,然后在自旋一次,如果还是获取不到锁,前驱节点为
//SIGNAL的waitStatus的情况下将会被挂起
doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly是定义在AQS抽象类中的通用代码,这段代码在第6节分析读写锁的时候进行了深入的分析,此处不再重入赘述,但是
大致步骤还是讲下:首先将线程包装成Node添加到同步队列中,然后会判断前驱节点是否为头节点,如果是头节点那么会再次进行一次获取锁的动作,因为在设置前驱节点
为SIGNAL之前有可能有线程释放了锁,然后在判断前驱节点是否为SIGNAL,如果不是将前驱节点设置为SIGNAL,然后在自旋一次,如果还是获取不到锁,前驱节点为
SIGNAL的waitStatus的情况下将会被挂起
获取共享锁的代码如下:
protected int java.util.concurrent.CountDownLatch.Sync#tryAcquireShared(int acquires) {
//如果计数等于零,那么返回1表示获取共享锁成功,否则失败
return (getState() == 0) ? 1 : -1;
}
我们在构建CountDownLatch的时候会传入一个计数值,当这个计数值被递减为零后,将会唤醒条件队列中的线程,下面是递减计数器的代码
public void java.util.concurrent.CountDownLatch#countDown() {
sync.releaseShared(1);
}
|
V
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//这段代码是AQS抽象类中的通用代码,我们在第6节分析读写锁的时候重点进行了分析
//大致逻辑是:检查头节点是否为SIGNAL的,如果是,那么会唤醒下一个节点,如果是0,那么会被设置为PROPAGATE,以便唤醒线程能够继续传播唤醒行为
doReleaseShared();
return true;
}
return false;
}
上面的doReleaseShared方法是AQS抽象类中的通用代码,我们在第6节分析读写锁的时候重点进行了分析,其
大致逻辑是:检查头节点是否为SIGNAL的,如果是,那么会唤醒下一个节点,如果是0,那么会被设置为PROPAGATE,以便唤醒线程能够继续传播唤醒行为
下面是释放共享锁的逻辑:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState
//如果已经是零了,那么没必要重复返回true,这样会导致线程重复的去检查是否需要唤醒下一个节点
if (c == 0)
return false;
//递减
int nextc = c-1;
//CAS改值,等于零的时候就可以唤醒同步队列中的线程了
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
CountDownLatch除了构造器之外,没有暴露重置计数器的方法,所以CountDownLatch是一次性用品,它的计数器被设置成零之后,其他线程再次调用它的await方法,不会被阻塞
三、总结
- CountDownLatch与CyclicBarrier从功能上来讲都是阻塞一批线程,然后通过达到某个条件之后以传播的方式唤醒所有的线程。
- 从实现上来看,CountDownLatch是直接实现的AQS框架,在计数没有被置零的情况下,可以有任意多的线程去获取共享锁,然后被阻塞,直到计数值被置零,然后以传播的方式
唤醒同步队列中被阻塞的线程,CountDownLatch是一次性的,它不像CyclicBarrier有reset方法,在CountDownLatch被置零之后,任意线程调用其await方法将不会再被阻塞 - CyclicBarrier是通过重入锁和Condition配置实现的,CyclicBarrier的容量是有限制的,其线程阻塞使用的队列为条件队列,在容量达到限制时,将会将条件队列中的线程
转移到同步队列中,最后通过某线程的unlock进行唤醒,由于从线程await的地方到unlock这段代码间没有做比较费时的操作,看起来依然像瘟疫一样快速传播,但是如果有额外
的线程介入的话,将会发生锁的竞争。不过额外介入的线程很快会调用await方法将自己挂起,如果运气不好那个因竞争失败的线程可能会被挂起,需要再次唤醒。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?