CountDownLatch 原理
一、CountDownLatch 原理
1、CountDownLatch在多线程并发编程中充当一个计时器的功能,并且内部维护一个count的变量,并且其操作都是原子操作,该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。
2、如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为0时,这时候阻塞队列中调用await()方法的线程便会逐个被唤醒,从而进入后续的操作。比如下面的例子就是有两个操作,一个是读操作一个是写操作,现在规定必须进行完写操作才能进行读操作。所以当最开始调用读操作时,需要用await()方法使其阻塞,当写操作结束时,则需要使count等于0。因此count的初始值可以定为写操作的记录数,这样便可以使得进行完写操作,然后进行读操作。
二、构造方法:CountDownLatch
内部也是有个Sync类继承了AQS,所以CountDownLatch类的构造方法就是调用Sync类的构造方法,然后调用setState()方法设置AQS中state的值
三、方法:countDown()
这个方法会对state值减1,会调用到AQS中releaseShared()方法,目的是为了调用doReleaseShared()方法,这个是AQS定义好的释放资源的方法,而tryReleaseShared()则是子类实现的,可以看到是一个自旋CAS操作,每次都获取state值,如果为0则直接返回,否则就执行减1的操作,失败了就重试,如果减完后值为0就表示要释放所有阻塞住的线程了,也就会执行到AQS中的doReleaseShared()方法
1、线程进入 countDown() 完成计数器减一(释放锁)的操作
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放共享锁
if (tryReleaseShared(arg)) {
// 释放锁成功开始唤醒阻塞节点
doReleaseShared();
return true;
}
return false;
}
2、更新 state 值,每调用一次,state 值减一,当 state -1 正好为 0 时,返回 true
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
// 条件成立说明前面【已经有线程触发唤醒操作】了,这里返回 false
if (c == 0)
return false;
// 计数器减一
int nextc = c-1;
if (compareAndSetState(c, nextc))
// 计数器为 0 时返回 true
return nextc == 0;
}
}
-
a、获取当前状态:调用 getState() 获取当前计数器的值。
-
b、检查计数器是否为零:如果计数器已经为零,则返回 false,表示不需要唤醒等待的线程。
-
c、减少计数器:将计数器减一,得到新的计数器值 nextc。
-
d、CAS 操作:使用 compareAndSetState(c, nextc) 尝试将计数器的值从 c 更新为 nextc。如果成功,则返回 nextc == 0,表示计数器是否减到零。
3、state = 0 时,当前线程需要执行唤醒阻塞节点的任务
private void doReleaseShared() {
for (;;) {
Node h = head;
// 判断队列是否是空队列
if (h != null && h != tail) {
int ws = h.waitStatus;
// 头节点的状态为 signal,说明后继节点没有被唤醒过
if (ws == Node.SIGNAL) {
// cas 设置头节点的状态为 0,设置失败继续自旋
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head,
// 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
if (h == head)
break;
}
}
小结:当线程使用 countDown() 方法时,其实是使用了 tryReleaseShared() 方法以 CAS 的操作来减少 state ,直至 state 为 0 ,进而释放锁资源,唤醒后续节点。
-
a、判断队列是否是空队列。
-
b、检查头节点的状态:如果头节点的状态为 Node.SIGNAL,则表示需要唤醒后续节点。
-
c、CAS 操作:使用 compareAndSetWaitStatus(h, Node.SIGNAL, 0) 尝试将头节点的状态从 Node.SIGNAL 更新为 0。如果成功,则调用 unparkSuccessor(h) 唤醒后续节点。
-
d、检查头节点是否变化:如果头节点没有变化,则退出循环。
在开始分析 doReleaseShared() 之前,我们先来补全一下 AQS 中 waitStatus 的状态说明
-
初始化状态:0,表示当前节点在同步队列中,等待获取锁;
-
CANCELLED:1,表示当前节点取消获取锁;
-
SIGNAL:-1,表示后续节点等待当前节点唤醒;
-
CONDITION:-2,表示当前线程正在条件等待队列中;
-
PROPAGATE:-3,共享模式,前置节点唤醒后续节点后,唤醒操作无条件传播下去;
四、方法:await()
1、线程调用 await() 等待其他线程完成任务:支持打断
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
2、AbstractQueuedSynchronizer#acquireSharedInterruptibly方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 判断线程是否被打断,抛出打断异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取共享锁,条件成立说明 state > 0,此时线程入队阻塞等待,等待其他线程获取共享资源
// 条件不成立说明 state = 0,此时不需要阻塞线程,直接结束函数调用
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
-
a、检查中断状态:首先检查当前线程是否被中断,如果被中断则抛出 InterruptedException。
-
b、尝试获取共享锁:调用 tryAcquireShared(arg) 方法尝试获取共享锁。在 CountDownLatch 中,tryAcquireShared 的实现是检查计数器是否为零,如果为零则返回 1,否则返回 -1。
-
c、如果获取锁失败:如果 tryAcquireShared 返回 -1,表示当前计数器不为零,调用 doAcquireSharedInterruptibly(arg) 方法将当前线程加入等待队列并阻塞。
3、线程进入 AbstractQueuedSynchronizer#doAcquireSharedInterruptibly 函数阻塞挂起,等待 latch 变为 0:
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 将调用latch.await()方法的线程 包装成 SHARED 类型的 node 加入到 AQS 的阻塞队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 前驱节点时头节点就可以尝试获取锁
if (p == head) {
// 再次尝试获取锁,获取成功返回 1
int r = tryAcquireShared(arg);
if (r >= 0) {
// 获取锁成功,设置当前节点为 head 节点,并且向后传播
setHeadAndPropagate(node, r);
p.next = null; // 到了这步时p节点会被删除
failed = false;
return;
}
}
// parkAndCheckInterrupt阻塞在这里
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
// 阻塞线程被中断后抛出异常,进入取消节点的逻辑
if (failed)
cancelAcquire(node);
}
}
-
a、将当前线程加入等待队列:调用 addWaiter(Node.SHARED) 将当前线程包装成一个 Node 节点并加入等待队列。哨兵节点--->Node节点
-
b、自旋等待:在一个无限循环中,检查当前节点的前驱节点是否是头节点。如果是头节点,则尝试获取共享锁。
-
c、获取锁成功:如果 tryAcquireShared(arg) 返回大于等于 0 的值,表示获取锁成功,调用 setHeadAndPropagate(node, r) 设置头节点并唤醒后续的共享节点。
-
d、检查是否需要阻塞:如果获取锁失败,调用 shouldParkAfterFailedAcquire(p, node) 检查是否需要阻塞当前线程。如果需要阻塞,则调用 parkAndCheckInterrupt() 阻塞当前线程并检查中断状态。
-
e、处理中断:如果在阻塞过程中线程被中断,则抛出 InterruptedException。
4、获取共享锁成功,进入唤醒阻塞队列中与头节点相连的 SHARED 模式的节点:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 将当前节点设置为新的 head 节点,前驱节点和持有线程置为 null
setHead(node);
// propagate = 1,条件一成立
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
// 获取当前节点的后继节点
Node s = node.next;
// 当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式
if (s == null || s.isShared())
// 唤醒所有的等待共享锁的节点
doReleaseShared();
}
}
-
a、将当前的头节点 head 赋值给局部变量 h,以便后续检查使用
-
b、将传入的 node 设置为新的头节点 head
-
c、这个条件判断包含了多个子条件,用于决定是否进行进一步的操作:
-
propagate > 0:如果 propagate 大于 0,说明state已经为0,表示需要传播信号。
-
h == null:如果旧的头节点为空,表示链表或队列之前是空的。
-
h.waitStatus < 0:如果旧的头节点的状态小于 0,表示该节点可能处于某种特殊状态(例如等待唤醒)。
-
(h = head) == null:再次检查新的头节点是否为空。
-
h.waitStatus < 0:再次检查新的头节点的状态。
-
-
d、当前节点是尾节点时 next 为 null,或者后继节点是 SHARED 共享模式
小结:当线程使用 await() 方法时会将当前线程封装成 node 加入AQS 队列中,如果发现 state 不为0,说明还有任务未执行完成,继续阻塞;如果 state 为0,会释放掉所有的等待线程,执行 await() 之后的数据。
5、doReleaseShared方法,唤醒队列中阻塞节点
private void doReleaseShared() {
for (;;) {
// 此时head已经为传入Node,因为在前面已经重置了
Node h = head;
// 判断队列是否是空队列
if (h != null && h != tail) {
int ws = h.waitStatus;
// 头节点的状态为 signal,说明后继节点没有被唤醒过
if (ws == Node.SIGNAL) {
// cas 设置头节点的状态为 0,设置失败继续自旋
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果有其他线程已经设置了头节点的状态,重新设置为 PROPAGATE 传播属性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 条件不成立说明被唤醒的节点非常积极,直接将自己设置为了新的head,
// 此时唤醒它的节点(前驱)执行 h == head 不成立,所以不会跳出循环,会继续唤醒新的 head 节点的后继节点
if (h == head)
break;
}
}
6、进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node 的 waitStatus 改为 -1,返回 false;waitStatus 为 -1 的节点用来唤醒下一个节点
//该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。
int ws = pred.waitStatus;
// 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回true
if (ws == Node.SIGNAL)
return true;
// 如果上一个节点的状态大于0,说明已经失效了
if (ws > 0) {
do {
// 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给 pred
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 一直找到小于等于0的
// 将重新标识好的最近的有效节点的 next 指向当前节点
pred.next = node;
} else {
// 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
-
a、shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)
-
b、当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true
-
c、进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
7、parkAndCheckInterrupt() 阻塞当前线程并检查中断状态
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程,如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// 判断当前线程是否被打断,清除打断标记
return Thread.interrupted();
}
案例:
package com.item;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class CountDownLatchtTest {
private static final int N = 10; // 乘客数
public static void main(String[] args) throws InterruptedException {
//step1:创建倒数闩,设置倒数的总数
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = Executors.newFixedThreadPool(N);
for (int i = 1; i <= N; ++i) {
e.execute(new Person1(doneSignal, i));
}
doneSignal.await(); //step2:等待报数完成,倒数闩计数值为0
System.out.println("人到齐,开车");
}
}
class Person1 implements Runnable{
private final CountDownLatch doneSignal;
private final int i;
Person1(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
@Override
public void run() {
//报数
System.out.println("第" + i + "个人已到");
doneSignal.countDown(); //step3:倒数闩减少1
}
}
五、流程图了解一下
六、CountDownLatch的文字描述
假设 CountDownLatch 初始计数器为 3(state = 3),以下是线程并发操作的动态过程:
步骤 1:初始化
-
状态:state = 3,CLH 队列为空。
-
线程 T1、T2、T3:调用 countDown() 减少计数器。
-
线程 W1、W2:调用 await() 等待计数器归零。
步骤 2:线程 W1 调用 await()
- a、检查计数器:state = 3 > 0,线程 W1 加入 CLH 队列并阻塞。
步骤 3:线程 T1 调用 countDown()
- a、减少计数器:state = 3 → 2
- b、未触发唤醒:state > 0,不唤醒队列中的线程
步骤 4:线程 W2 调用 await()
- a、检查计数器:state = 2 > 0,线程 W2 加入 CLH 队列并阻塞。
步骤 5:线程 T2 调用 countDown()
- a、减少计数器:state = 2 → 1
- b、未触发唤醒:state > 0,不唤醒队列中的线程
步骤 6:线程 T3 调用 countDown()
- a、减少计数器:state = 1 → 0
- b、触发唤醒:state == 0,唤醒 CLH 队列中的所有线程
- c、线程 W1、W2 被唤醒:继续执行后续逻辑