CountDownLatch是一个同步工具类,为我们提供了一种并发流程的控制手段,它允许一个或多个线程等待其他线程完成操作。使用指定的数(也就是计数器,大于等于零)初始化CountDownLatch之后,每当调用countDown方法时,计数器就会减1,如果计数器的值还大于0,CountDownLatch的await方法会阻塞当前线程,直到计数器变为0,就会释放所有等待的线程。await方法后面的逻辑也将在所有释放的线程均执行完成之后得以执行,但计数器不能被重置。
一、主要方法介绍
CountDownLatch类主要的方法如下:
1、构造器
(1)CountDownLatch(int count),利用指定的计数器创建一个CountDownLatch,该类只有这一个构造器。
2、方法
(1)void await(),让当前线程在计数器到达零之前一直处于等待状态,也就是等待计数器到达0,该方法是CountDownLatch常用的方法之一。
(2)void countDown(),使计数器减1,如果计数器到达0,将释放所有等待的线程,该方法是CountDownLatch常用的方法之一。
(3)boolean await(long timeout, TimeUnit unit),让当前线程在计数器到达零之前一直处于等待状态,除非超过了等待的时间。
(4)long getCount(),获取当前计数器的值。
二、CountDownLatch的使用
CountDownLatch主要用来协调多个线程之间的同步,它可以让一个或多个线程在完成一些正在其他线程中执行的逻辑之前一直处于等待状态,类似于join()方法。也可以理解为应用程序的主线程希望在负责启动框架服务的线程已经完成之后再执行。比如:文件上传与下载,可以采用多个线程去上传或者下载,只用所有的线程都完成了上传或者下载之后,才能提示上传或者下载成功。
1、使用join()方法实现上传
public class JoinDemo {
public static void main(String[] args) {
try {
System.out.println("----------- 开始上传 ------------");
Thread task1 = new Thread(new UploadFile());
Thread task2 = new Thread(new UploadFile());
task1.start();
task2.start();
task1.join();
task2.join();
System.out.println("----------- 上传结束,进行之后的工作 ------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class UploadFile implements Runnable{
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " : 正在上传");
// 让线程睡觉,模拟上传
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :上传完了");
}
}
}
执行结果如下图,注意打印的结果中,线程执行线束的时间与线程开始的时间无关:
1、使用CountDownLatch实现上传
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
System.out.println("----------- 开始上传 ------------");
try {
for (int i = 0; i < 3; i++) {
Task task = new Task(countDownLatch);
Thread thread = new Thread(task);
thread.start();
}
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----------- 上传结束,进行之后的工作 ------------");
}
static class Task implements Runnable{
private CountDownLatch countDownLatch;
public Task(CountDownLatch countDownLatch){
this.countDownLatch = countDownLatch;
}
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " : 正在上传");
// 让线程睡觉
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " :上传完了");
countDownLatch.countDown();
}
}
}
执行结果如下图:
可以看到主线程在所有子任务执行完前必须在闭锁上等待,只有三个线程都执行完成之后才会继续往下执行。
三、 CountDownLatch与join 方法 的区别
(1)CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕。
(2)countDownLatch 相比 join 方法让我们对线程同步有更灵活;使用线程池来管理线程时候,一般都是直接添加 Runable 到线程池,这时候就没有办法在调用线程的 join 方法。
四、CountDownLatch与CyclicBarrier的区别
CountDownLatch与CyclicBarrier的区别主要体现在以下三方面:
1、CountDownLatch
(1)不可重复使用,也就是计数器不能被重置
(2)强调一个或多个线程等待另外N个线程执行之后才能执行
(3)通过AQS实现
2、CyclicBarrier
(1)可以重复使用
(2)强调N个线程相互等待,只要有一个未完成,所有的线程都需要等待
(3)通过ReentrantLock实现
五、CountDownLatch源码简述
1、CountDownLatch构造器
首先,来看一下创建CountDownLatch的过程,CountDownLatch底层是通过AQS实现的,它内部维护了一个继承自AbstractQueuedSynchronizer的Sync静态内部类。实际上,指定的计数器的值保存在AbstractQueuedSynchronizer的同步状态state属性中,也就是在初始化CountDownLatch时,内部又调用了内部类Sync的构造器,然后将计数器的值赋给state,以此来实现阻塞的机制。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch构造器的源码如下,通过源码可以看出,先判断计数器count的值,可以发现计数器的值可为0,但是不建议这样使用,因为计数器的值为零时,CountDownLatch的其他方法完全不起作用,不能阻塞线程。接下来就是直接调用Sync的构造器,Sync的构造器通过set方法将count赋值给了同步状态state,用来表示达到条件的线程数量:
/**
* 设置同步状态state的值
*/
Sync(int count) {
setState(count);
}
2、阻塞方法await()
接下来看await()方法的实现,这里为了方便说明各方法的实现,将await()涉及到的方法都放在了一起,下文中的countDown()方法也采用同样的方式。
该方法内部直接调用父类提供的acquireSharedInterruptibly方法,该方法中先判断当前线程是否已经被中断,如果当前线程已经被中断了,那就抛出异常,如果没有,判断子类Sync中tryAcquireShared返回值是否小于0,从tryAcquireShared方法可以看到实际上判断的是同步状态是否等于0,也就是达到条件的线程数量。例如,前文中初始化时传入了3,当前同步状态是否等于0时就表示达到条件的线程数量已经为3了,那就进行下一步操作;如果达到条件的数量不为3,那么该方法便返回1,也就不进行下一步操作了。
其中,doAcquireSharedInterruptibly方法中涉及的相关逻辑属于AQS的内容了,该方法中不要利用park加上自旋,结合CAS来保证在并发情况下,线程要么获得锁(共享锁),要么被添加到等待队列等待被唤醒。本文中不做详细的介绍,后期会写专门介绍AQS的文章。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AbstractQueuedSynchronizer的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 内部类Sync的方法,由父类的acquireSharedInterruptibly方法调用
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
3、释放状态方法countDown()
每调用一次countDown()方法,如果count值大于0,那么计数器就会减1,表示达到条件的线程又多了一个,如果计数器就会减1之后的值为0,也就是所有的线程都达到条件了,将会唤醒正在等待的线程。如下面的代码片段,countDown()方法也是直接调用父类的releaseShared()方法,releaseShared()方法中又对tryReleaseShared方法的返回值进行判断,如果返回true的话将会唤醒等待中的线程,也就是唤醒因调用了await()导致阻塞的线程。那么在什么情况下tryReleaseShared()方法会返回true呢?
从tryReleaseShared()方法中可知,该方法通过循环(自旋)减少计数器count的值。这个过程中,先获取同步状态state的值,如果同步状态为0,那么直接返回false,最终countDown方法也直接返回,什么事都不做。如果同步状态不为0,那么将state的值减1,然后,再通过CAS将state的减1,如果CAS设置state的值失败,那么将再一次循环,完成同样的逻辑直到成功将state的值减1,设置成功之后将判断计数器是否为0,如果为0,就返回true,那就说明当前线程是最后一个调用countDown()方法的线程,也就是所有的线程都达到条件了,那就唤醒调用了await 方法而被阻塞的线程线程。同样,本文中不解释唤醒的详细过程。
public void countDown() {
sync.releaseShared(1);
}
// AbstractQueuedSynchronizer的方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 内部类Sync的方法
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
六、参考资料
【1】张振华 Java并发编程从入门到精通[M] 清华大学出版社
【2】放腾飞 魏鹏 程晓明 Java并发编程的艺术[M] 机械工业出版社
【3】https://blog.csdn.net/qq_38462278/article/details/82842129
【4】https://blog.csdn.net/qq_28822933/article/details/83340642