CountDownLatch
一般称为闭锁
、计数器
,是一种多线程同步工具,属于 AQS 体系的一员。
常用于让协调线程等待一组工作线程全部“完成工作“或“满足特定条件"后继续进行下去。
但其实也可以和 CyclicBarrier
让一组线程全部到达指定点后才继续执行,不过不如 CyclicBarrier
简单且不可重用,所以一般一组线程自等待的场景我们倾向于直接使用 CyclicBarrier
。
老板有个保险箱,为了保证安全,指定 3 个亲信拿着密码。只有当 3 个亲信同时输入密码时,老板才能打开保险箱。
哪天老板说要取钱跑路了,赶紧开锁。所以亲信赶紧输入密码。
我们可以用让老板用 int 做计数器,一直循环等待完成:
private static void rr() {
System.out.println("老板:我要开锁");
AtomicInteger count = new AtomicInteger(3);
for (int i = 0; i < count.get(); i++) {
new Thread(new Runnable() {
@Override
public void run() {
count.decrementAndGet();
}
}).start();
}
while(count.get() != 0) {
System.out.println("老板:几个傻子给我快点");
Thread.sleep(1000);
}
System.out.println("老板:拿钱跑路, 亲信卖了");
}
但是呢,会有几个问题?
简单的 int 会有并发问题,我们要么用 volatile 修饰,要么使用原子类
老板一直催催催,很烦的,需要让他安静一下,就要让他睡一会。
可是保险箱的锁在第一次输入后,就要在限定时间内全部完成并打开,那老板睡过了怎么办。
是不是还需要其他的线程通信手段呢?
亲信 1:报告老板,咱发现一个好东西。既可以安全的输入密码,几个人不会错乱
亲信 2:全部输入之后,马上就通知您了。
亲信 3:还支持限定时间内不完成,也会通知到您出问题了。(OS:指不定我们哪个被人给“灭口”了)。
老板:好东西,搞出来给我看看。
public class CountDownLatchDemo {
public static void main(String[] args) {
System.out.println("老板:我要开锁");
// 指定 3 个亲信
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(new Follower(latch)).start();
}
try {
latch.await(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
System.out.println("老板:都是傻子吗,这都输入失败了");
}
System.out.println("老板:拿钱跑路, 亲信卖了");
}
private static class Follower implements Runnable {
private CountDownLatch latch;
public Follower(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println("亲信:输入密码ing");
latch.countDown();
System.out.println("亲信:报告老板, 输入完成");
}
}
}
老板:我要开锁
亲信:输入密码ing
亲信:报告老板, 输入完成
亲信:输入密码ing
亲信:报告老板, 输入完成
亲信:输入密码ing
亲信:报告老板, 输入完成
老板:拿钱跑路, 亲信卖了
苦于老板的压榨,亲信们走上了截然不同的道路。
亲信 OS:老板终究会把我们卖了,我们自己翻身做主人吧,把钱给拿了。
private static void cycleCountDown() throws InterruptedException {
System.out.println("亲信们:不成功便成仁!!!!");
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(new MyFollower(latch)).start();
}
Thread.sleep(1000);
}
private static class MyFollower implements Runnable {
private CountDownLatch latch;
public MyFollower(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println("亲信:输入密码ing");
latch.countDown();
System.out.println("亲信:我这边搞定了,等你们.");
try {
latch.await(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
System.out.println("OS:你们还想不想要钱了");
}
System.out.println("亲信:成了!!!!!");
}
}
亲信们:不成功便成仁!!!!!
亲信:输入密码ing
亲信:输入密码ing
亲信:我这边搞定了,等你们.
亲信:我这边搞定了,等你们.
亲信:输入密码ing
亲信:我这边搞定了,等你们.
亲信:成了!!!!!
亲信:成了!!!!!
亲信:成了!!!!!
主要方法如下,非常简洁:
最最关键的还是 Sync
的内部类,实现了 AbstractQueuedSynchronizer(AQS)。
这里不再说明 AQS 的内容,会简单提一下,深入研究参见:AbstractQueuedSynchronizer(AQS):并发工具的基石 (juejin.cn)
CountDownLatch
内部会持有一个 Sync
的实例:private final Sync sync;
在构造时指定数量的同时,去构建这个 Sync
:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync
接受到这个计数后,那了解 AQS 的同学马上就会明白,这是要赋予 state 这个字段真实的含义:剩余需达到指定条件的数量。
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
}
await 有两个重载方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
这里就是 AQS 的范畴了:
而前一个步骤是 Sync
需要实现的方法。
因为需要对 state 这个计数值进行判断,但是这个计数是在子类才赋予的含义,所以操作方法也在子类:
只有计数 state = 0 才能继续进行。
private static final class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
}
那如果没到 0,就让老板等着。AQS 内部维护了一个等待资源的队列,延伸到子类,那就是等待 getState() == 0
的情况发生。
亲信:既然老板不比比了,就要赶紧输入密码,不然超时又要吵了。
亲信 --;亲信 --;亲信–。完成!!!
public void countDown() {
sync.releaseShared(1);
}
看看,看看,又是 AQS:
上面说到,如果是简单的 int,容易出现并发问题,导致输入混乱,那最后还得挨老板批。
所以需要可靠的方式:CAS + volatile(state 本质是 volatile 修饰的)
private static final class Sync extends AbstractQueuedSynchronizer {
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
// 已经完成不能再通知了,一次性的
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
// 恰好完成,返回 true,以便进行通知。
return nextc == 0;
}
}
}
返回 true,就可以唤醒老板了:老板老板,输入完成!!!
其实省略了很多 AQS 的内容,这东西复杂也挺复杂,搞明白也挺好明白。一言两语说不清,不如看看这两篇文章
可是还有问题,这玩意是一次性的。
平行时空 1:
老板:这玩意只能用一次有个屁用,我这是保险箱,不是射火箭!!!
CountDownLatch
:射火箭我也可以的,毕竟火箭发射掌握在几个总工程师手上呢~亲信:
平行时空 2:
亲信们:这玩意咋坏了,怎么让老板短期不发现啊
CyclicBarrier
:听说有人在 cue 我?