有时候我们会有这样的场景,需要等待几个异步线程,同时完成后程序才可以放行,进行下一步操作。因此jdk JUC包下提供了CountDownLatch闭锁,正如名字一般,倒计数直到为0时放行。除此之外 JUC包下还有一个CyclicBarrier类也能提供类似功能,与前者区别是可以重复使用。当然了,为了达到这个目的我们还有很多方法,不过这不是我们本文讨论的范围内。
1
用团购的方式理解闭锁CountDownLatch
相信大家都有过团购的经历,非常便宜!咳咳,不知大家有没有关注过,团购页面上总会有这样的显示:60人成团,还差5人之类的文字。
这个就和我们的CountDownLatch很像了,店家会先设置某样商品多少人成团(类似于CountDownLatch构造函数参数count),之后顾客就可以拼团了。当第一个顾客点击拼团支付后,顾客需要保持等待成团(相当于CountDownLatch await方法),同时系统成团人数就还差60-1=59人(计算相当于CountDownLatch的countDown方法)。由于人数还不满足拼团,顾客只能保持等待。之后类似的不断有顾客进入了,直到计数为0时,整个团购就结束了。当然这个等待过程中,有些顾客会等不及,就取消拼团了(这就类似await方法,有了时间参过期参数)。
其实现方式也非常简单,是基于java提供的AQS同步器框架实现的,因此只要理解了AQS框架的原理,再来学习CountDownLatch就非常简单了。对于还不了解AQS框架的可以先阅读以下两篇文章:
其实现与信号量类似也使用了文章(二)中共享锁的逻辑。现在我们来看一下。
2
闭锁CountDownLatch源码
对于CountDownLatch而言,我们主要关注的方式是await和countDown两个方法,等待和倒数。
构造函数:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count); // 继承AQS同步器
}
count: 计数大小。类似于团购成团要求人数。
等待拼团:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 会调用tryAcquireShared方法
}
// CountDownLatch实现的AQS子类
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1; // 判断拼团剩余人数 =0时放行,否则等待。
}
await方法逻辑简单(等待拼团,当剩余人数为0时,直接通行否则等待)。
平台拼团剩余人数倒数:
public void countDown() {
sync.releaseShared(1); // 会调用tryReleaseShared方法
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) { // 保证cas可以执行
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc)) // 平台成团剩余人数,cas保证并发安全。
return nextc == 0; // 是否人数为0,如果是0成团,开始唤醒等待拼团的人(doReleaseShared)
}
}
countDown方法,主要是为了修改拼团成功的剩余人数,只是为了保证并发安全,使用CAS。最后修改完成后判断人数是否等于0,如果等于0就可以通知所有的顾客,拼团已经成功了。
ps:无论是信号量还是闭锁的实现,都是通过继承AQS框架来实现同步的。而且只用了非常少的代码,可见AQS框架真的是amazing。
- End -