CountDownLatch倒计时锁
CountDownLatch类结构
设计目标:让一个或多个线程等待其他线程完成特定操作(例如初始化任务、并行计算等)。
核心语义:state 表示 剩余需要等待的计数,而非可用资源数。当 state 减到 0 时,表示所有被等待的操作已完成,等待线程可以继续执行
public class CountDownLatch {
//内部类定义 也是继承AQS 所以也是依靠AQS实现功能的
private final Sync sync;
private static final class Sync extends AbstractQueuedSynchronizer {}
//构造方法就这一个
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
....剩余部分方法体这里省略 在下面讲.............
}
类结构是非常简单的,一个构造参数,类没有继承 没有实现,依靠一个继承了AQS内部类
内部类Sync涉及方法
1.setState(),getCount()
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // 初始化 AQS 的 state 为计数器的初始值
}
int getCount() {
return getState(); // 当前剩余的计数
}
和Semaphore一样,初始的state计数器的值也是通过构造方法传进来的,说明这个state最初也是由调用者决定的,需要注意的是state的定义和Semaphore中state定义不同
Semaphore :state 表示当前可用的资源数,当 state > 0 时线程可以获取资源(许可证) 否则需等待
CountDownLatch :state 表示 剩余需要等待的计数(例如需等待的线程数),而非可用资源数,state 减 1,直到 state 归零,当 state 归零时,表示所有被等待的操作已完成,等待线程可以继续执行。
2.tryAcquireShared(int acquires)
复写的AQS钩子方法,这里的参数int acquires在CountDownLatch类中没有用到,CountDownLatch类中的关于计数器值的参数很多都是默认的 比如sync.acquireSharedInterruptibly(1)
//尝试获取共享锁:当 state 为 0 时成功(返回 1),否则失败(返回 -1)
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
核心逻辑 检查 AQS 的 state 值:直接读取 state(即 CountDownLatch 的计数器值)
- 若 state == 0,返回 1,表示当前线程可以获取共享锁(即 state 已归零,无需阻塞)
- 否则返回 -1,表示获取失败, 需要阻塞当前线程(即 state 未归零,等待其他线程调用 countDown()方法:减少计数器值,当计数器归零时唤醒所有等待线程)
3.tryReleaseShared(int releases)
// 尝试释放共享锁:通过CAS将state减1,当减到0 时返回 true
protected boolean tryReleaseShared(int releases) {
for (;;) {//自旋检查 只有CAS操作成功才会退出,解决多线程并发修改state的竞争问题
int c = getState();
if (c == 0) return false; // 已为0,无法释放
int nextc = c - 1;
if (compareAndSetState(c, nextc)) // CAS更新state
return nextc == 0; // 仅当state减为0时返回true
}
}
步骤
- 获取当前 state 值 调用 getState() 获取计数器当前值。
- 检查是否已归零 若 state == 0,直接返回 false,终止操作。
- 计算新值 nextc 将 state 减 1,得到 nextc = c - 1。
- CAS 更新 state 原子性地将 state 从 c 更新为 nextc,失败则重试。
- 判断是否触发唤醒 若 nextc == 0,返回 true 触发 AQS 唤醒等待线程(仅在计数器归零时唤醒);否则返回 false
核心说明:
①.多线程同时调用 countDown() 可能导致 state 更新不一致。自旋方式,若 CAS 操作失败(其他线程已修改 state),则重新进入循环,重新获取最新值并重试
②CAS更新state成功 返回是的 nextc == 0的布尔值,返回 true,通知 AQS 需要唤醒等待线程,否则不通知,也就是**仅在计数器归零时唤醒,**避免过早唤醒导致逻辑错误
核心方法
1 构造函数
初始化时设置计数器的初始值
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
2 await()
阻塞当前线程,直到计数器归零(支持中断)
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 调用 AQS 的共享可中断获取方法
}
acquireSharedInterruptibly方法是AQS 的共享可中断获取方法,而且参数默认也是 1
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (tryAcquireShared(arg) < 0) // 调用 tryAcquireShared 判断是否有可以使用的锁
doAcquireSharedInterruptibly(arg); //加入等待队列并尝试获取锁
}
调用Sync的tryAcquireShared(arg)方法,该方法只有两个返回值 1 和 -1
如果是1 代表当前state为0 说明没有需要等待完成任务的线程,不需要阻塞,await啥也没干到这结束
如果-1<0 说明 当前state没有归0,说明当前等待完成任务的线程还有,那就进入下面方法里
doAcquireSharedInterruptibly(arg)
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException {
//将当前线程包装成共享模式的节点(Node.SHARED),通过 addWaiter 方法加入同步队列尾部。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {//自旋尝试获取资源
// 获取前驱节点Prev 并同时做了判空处理,如果前驱节点为空 抛出空指针异常
final Node p = node.predecessor();
//判断前驱是否为头节点
if (p == head) {
//只有前驱是头节点时,当前节点才有资格尝试获取资源
int r = tryAcquireShared(arg);
if (r >= 0) {
//将当前节点设为新头节点,并根据剩余资源量(propagate)决定是否继续唤醒后续共享节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 如果不是前驱节点不是头节点 就要检查是否阻塞线程挂起线程等待操作
//调用 shouldParkAfterFailedAcquire 检查是否需要阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
//若需要,则调用 parkAndCheckInterrupt 挂起线程
parkAndCheckInterrupt())
//若在挂起期间线程被中断,parkAndCheckInterrupt 返回 true
throw new InterruptedException();
}
} finally {
if (failed)
//抛出 InterruptedException,并调用 cancelAcquire(node) 取消节点获取(清理节点状态,移除无效节点
cancelAcquire(node);
}
}
这个方法实现步骤
①包装成共享模式节点 加入同步队列
②开始自旋获取资源
获取前驱节点Prev 并同时做了判空处理
Ⅰ 如果前驱节点是头节点,尝试节点获取资源 方法还是tryAcquireShared(arg)
Ⅱ 成功获取资源 调用 setHeadAndPropagate(node, r),将当前节点设为新头节点并根据剩余资源量(propagate)决定是否继续唤醒后续共享节点
Ⅲ 清空原头链接指向关系 退出循序 结束获取流程
如果前驱节点不是头节点,那么当前线程就在同步队列中需要等待的,这个时候调用 shouldParkAfterFailedAcquire 检查是否需要阻塞线程。若需要,则调用 parkAndCheckInterrupt 挂起线程(挂起的就是当前的线程 即node.thread)代码就停在park处不在继续往下执行了
③处理中断
若在挂起期间线程被中断,中断触发解挂,恢复从park处执行,然后parkAndCheckInterrupt 返回 true,抛出 InterruptedException,那就退出循环了 并且finally 代码块的执行标识 依然是ture ,开始调用调用 cancelAcquire(node) 取消节点获取(清理节点状态,移除无效节点) 意思就是如果挂起操作被中断了,说明当前线程不需要挂起,从队列中讲节点清理出去 顺便将无效节点也清空
setHeadAndPropagate在当前线程获取资源后,更新队列头节点并传播唤醒后续等待线程
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 记录旧头节点
setHead(node); // 设置新头节点
// 判断是否需要传播 这里的propagate = 1 int propagate是 r = tryAcquireShared(arg)为1
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 唤醒后续节点 这是自旋方法
}
}
//doReleaseShared()方法
private void doReleaseShared() {
for (;;) {
Node h = head;//走到这里在await方法传入的节点已经为头节点了
if (h != null && h != tail) { // 队列非空
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 尝试将头节点状态从 SIGNAL 改为 0,成功则唤醒后续节点
if (compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
//注意unparkSuccessor(h) 解挂的是LockSupport.unpark(node.next.thread);
//恢复后续节点(线程)停留在await方法的park处执行,解挂继续执行 继续await的自旋方法
unparkSuccessor(h);
}
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
// 状态为0时尝试标记为 PROPAGATE,失败则重试
continue;
}
}
// 头节点未变化则退出,否则继续循环处理新头节点
if (h == head) break;
}
}
setHeadAndPropagate 通过动态设置头节点和智能判断传播唤醒后续节点尝试获取锁从而不停循环条件,优化了共享资源的分配效率,避免线程因资源充足时仍无谓等待
这里如何避免多线程竞争不安全问题,比如线程A 线程B都执行doReleaseShared方法,但是取的队头是同一个,这样就对后执行完的造成问题,解决方法就是设置状态PROPAGATE传播状态上
头节点状态有两种 唤醒SIGNAL 或者 默认0,刚成为头节点的状态是唤醒后续节点的状态,在要去执行唤醒后续节点操作unparkSuccessor(node.next)时候,状态变更为0
通过判断如果头节点状态为0 ,线程A将状态从0改为PROPAGATE,那这时候其余线程比如线程B在尝试修改状态就会失败,compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 因为这个时候状态是传播状态PROPAGATE了,那就会退出此次循环 需要线程B自旋重新找下最新的头节点再开始操作
3 countDown()
减少计数器值,当计数器归零时唤醒所有等待线程
public void countDown() {
sync.releaseShared(1); // 调用 AQS 的共享释放方法
}
可以看到这个方法默认参数是1 也就是执行一次 理论上减少1次计数器
releaseShared(1)方法是AQS的共享释放方法 有两部
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
1.tryReleaseShared(arg))还是走的上面的sync的方法 这里不复述
2.如果判断条件成立,返回ture,也就是nextc == 0 剩余计数器为0了,那开始执行doReleaseShared()方法,这就又回到await方法执行的底层执行方法了 恢复执行解挂恢复到await的park处的线程 闭环喽
# CountDownLatch 共享模式的工作流程
简单代码示例流程
// 主线程等待两个工作线程完成任务
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
// 工作线程 1
new Thread(() -> {
System.out.println("线程1完成任务");
latch.countDown();
}).start();
// 工作线程 2
new Thread(() -> {
System.out.println("线程2完成任务");
latch.countDown();
}).start();
// 主线程等待
System.out.println("主线程开始等待...");
latch.await();
System.out.println("所有子线程任务完成,主线程继续执行");
}
}
在这段代码里
步骤1:依靠AQS实现的同步器,初始化state = 2(需要等待 2 个线程完成任务),一个null的同步队列
步骤2:代码里有三个线程,一个主线程main,两个自线程,三个线程都在CountDownLatch 监管
Ⅰ.主线程开始启动,按照执行顺序 先后启动两个子线程,然后执行到await方法,发现state不为0,那就等加入到条件队列等在这了 (这个时候可以debug看下CountDownLatch 的队列值),
Ⅱ.这时候两个子线程被启动后先后执行,并执行latch.countDown()方法,因为state=2,第一个线程执行latch.countDown();发现计数器减去一后不归零 就不触发doReleaseShared方法执行,第二个线程执行latch.countDown();发现计数器再减去一后归零,触发doReleaseShared方法执行,
Ⅳ.从doReleaseShared方法到方法里面的unparkSuccessor(h);唤醒后续节点的线程, 然后这就又跳到 await方法里面的挂起线程的方法parkAndCheckInterrupt,从该方法的park处恢复执行,然后继续自旋执行 直到if (h == head) break;当前节点就是头节点,退出doReleaseShared方法,再退出setHeadAndPropagate方法 然后再退出await方法的自旋从而退出await方法
场景额外示例验证上述步骤思考
①如果将上面代码await方法放在两个子线程前面,就会发现一直卡着了,没有机会释放计数器值了
②如果将上面代码state值改为3,那么就会发现会一直卡在await方法了,不在执行了
③如果在上述子线程其中一个线程里放await方法 会发现另外一个子线程先执行完就卡着了,除非执行的那个子线程里面有多次countDown()行为,至少countDown()次数 大于等于 state值
初始化:state 设置为初始计数值
await():检查 state 是否为 0,若否则线程进入等待队列。
countDown():通过 CAS 将 state 减 1,减至 0 时唤醒所有等待线程。
唤醒后:所有等待线程继续执行后续逻辑。