同步工具类:CountDownLatch
- 介绍
- 源码分析
- 继承图
- 核心方法分析
- await()
- countDown()
- 业务场景
- 代码实现
- 测试结果
- 总结
介绍
Jdk原文翻译
CountDownLatch 一种同步辅助工具,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
CountDownLatch用给定的计数初始化。由于countDown方法的调用,await方法会阻塞,直到当前计数达到零,然后释放所有等待线程,然后立即返回await的任何后续调用。这是一种一次性现象——计数无法重置。如果您需要重置计数的版本,请考虑使用CyclicBarrier。
CountDownLatch是一种通用的同步工具,可用于多种目的。用计数1初始化的CountDownLatch用作一个简单的开/关锁存器或门:所有调用countDown的线程都在门处等待,直到它被调用countDown的线程打开。初始化为N的CountDownLatch可用于使一个线程等待,直到N个线程完成某个操作,或某个操作完成N次。
CountDownLatch的一个有用的特性是,它不要求调用countDown的线程在继续之前等待计数达到零,它只是防止任何线程通过等待,直到所有线程都可以通过。
通俗理解
CountDownLatch 是一个多线程同步工具类,在多线程环境中它允许多个线程处于等待状态,直到前面的线程执行结束,从类名上看CountDown既是数量递减的意思,我们可以把它理解为计数器。
源码分析
继承图
核心方法分析
await()
如下所示,await() 调用的是AQS的模板方法。然后 CountDownLatch.Sync 重新实现了 tryAccuqireShared方法。
从tryAccuqireShared 方法的实现来看,只要 state!=0,调用 await()方法的线程便会被放入AQS的阻塞队列,进入阻塞状态。
public void await() throws InterruptedException {
//调用 AQS 的模板方法
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果其他线程调用interrupt ,也可以唤醒 此线程
if (Thread.interrupted())
throw new InterruptedException();
//CountDownLatch.Sync 重新实现了 tryAcquireShared 方法
if (tryAcquireShared(arg) < 0)
//只要state!=0,就会放入阻塞队列进行阻塞,进入阻塞状态
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;//
}
countDown()
countDown() 调用的AQS的模板方法 releaseShared(),里面的tryReleaseShared()被 CountDownLatch.Sync重新实现。
从下面的代码可以看出,只有state 的状态成功被改成了0。tryReleaseShared()方法才会返回true.然后执行doReleaseShared()。一次性唤醒队列中所有阻塞的线程。
public void countDown() {
//调用AQS的模板方法
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//CountDownLatch.Sync 重新实现了 tryReleaseShared 方法
if (tryReleaseShared(arg)) {
//唤醒队列中所有阻塞的线程
doReleaseShared();
return true;
}
return false;
}
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;
//只有 c==1, 减一后变成0.然后将 c 的值改成0.此时才返回true.
//然后唤醒阻塞队列中的所有阻塞线程
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
业务场景
有个人脸识别应用,比如我们常见的上班打卡应用。应用首次启动要求多线程将存储在DB中的人脸数据加载到本地应用中,主线程需要等待所有子线程完成任务后,才能继续执行余下的业务逻辑,比如加载 dubbo组件啥的。
代码实现
此处使用了两个CountDownLatch ,一个是用于准备DB读取的准备工作,另一个是等待DB读取工作完成后开始加载其他组件。
public class Demo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(10);
for (int i = 0; i < 10; ++i) // create and start threads
new Thread(new DoDemo(startSignal, doneSignal)).start();
doSomethingElse();
//开始加载数据,
startSignal.countDown();
//主线程阻塞,等待数据加载完成
doneSignal.await();
doSomethingElse();
System.out.println("数据加载完成,开始启动其他组件,包括dubbo组件");
}
public static void doSomethingElse(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class DoDemo extends Thread {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
public DoDemo(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
@Override
public void run() {
try {
//开始阻塞了,等待主线程开启
System.out.println(Thread.currentThread().getName()+" 开始阻塞等待了...");
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {
}
}
public void doWork() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " 开始干活了,DB 中的数据加载到本地缓存中");
Thread.sleep(1000);
}
}
测试结果
总结
多线程并发的情况下需要做好同步处理,结合CountDownLatch 充分的运用到业务场景当中还是挺有必要的,凡是需要在多个任务执行完成后再去做另一件事的情况都可以考虑使用CountDownLatch。合理使用但请不要滥用,特别上面也提到过计数值需要确定,否则可能导致多任务无法做到同步甚至造成主线程无限等待。