CountDownLatch
CountDownLatch
是一个线程同步工具,能够协调多个线程的同步。能够使当前线程等待其他一个或多个线程执行完毕后自动恢复执行。它相当于是一个计数器,初始化时指定需要执行的线程数量,每个线程结束之后计数器 -1
,到计数器为 0
时,在恢复此前阻塞的线程。在一些场景如果不借助这种工具,代码可能便不那么「优雅」。
举个例子
有这样一个需求,需要在两个线程处理数据,在组合两个结果之后处理后续逻辑,如果不用 CountDownLatch
,可能会这样写:
java
public class ThreadTest7 {
volatile int seq = 0;
public static void main(String[] args) {
ThreadTest7 test7 = new ThreadTest7();
new SomethingProcessor("thread1", value -> {
test7.seq++;
System.out.println("callback:" + value);
test7.tryDoSomethingElse();
}).start();
new SomethingProcessor("thread2", value -> {
test7.seq++;
System.out.println("callback:" + value);
test7.tryDoSomethingElse();
}).start();
}
private void tryDoSomethingElse(){
if(seq == 2){
System.out.println("Doing something!");
}
}
}
class SomethingProcessor extends Thread{
private String name;
private ProcessorCallback callback;
SomethingProcessor(String name,ProcessorCallback callback){
this.name = name;
this.callback = callback;
}
@Override
public void run() {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
callback.onCallback(name + "Fake processor!");
}
interface ProcessorCallback{
void onCallback(String value);
}
}
=========================================================
callback:thread1Fake processor!
callback:thread2Fake processor!
Doing something!
这样的代码应该也并不少见,业务逻辑是需要两个线程都处理完毕之后再组合处理之后的逻辑。而处理任务的两个线程相互不通讯,无法知道其他线程的状态,所以两个线程都需要去调用后续方法,在这个方法内回去判断是不是所有线程都执行完毕,如果是则继续,不是则 retrun
。
代码看起来没问题,性能也ok,不过不太「好看」。现在只有两个线程,所以你需要执行两次 tryDoSomethingElse()
,那如果是十个八个线程呢?是不是感觉就头疼了?而且麻烦的是,如果调用 tryDoSomethingElse()
就意味着你的业务线程需要耦合其他逻辑,这便增加了代码耦合度,背离了我们代码架构的准则。
如果这时候,你用 CountDownLatch
,则可以更「漂亮」地解决问题,来看下如何改造:
java
public class ThreadTest7 {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
ThreadTest7 test7 = new ThreadTest7();
new SomethingProcessor("thread1", value -> {
System.out.println("callback:" + value);
}, latch).start();
new SomethingProcessor("thread2", value -> {
System.out.println("callback:" + value);
}, latch).start();
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
test7.doSomethingElse();
}
private void doSomethingElse() {
System.out.println("Doing something!");
}
}
class SomethingProcessor extends Thread {
private String name;
private ProcessorCallback callback;
private CountDownLatch countDownLatch;
SomethingProcessor(String name, ProcessorCallback callback, CountDownLatch latch) {
this.name = name;
this.callback = callback;
countDownLatch = latch;
}
@Override
public void run() {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
callback.onCallback(name + "Fake processor!");
countDownLatch.countDown();
}
interface ProcessorCallback {
void onCallback(String value);
}
}
可以看到,我们去掉了计数器 seq
,声明一个 CounDownLatch(2)
注入到子线程。这样,在子线程的回调中就不需要去管后续操作是什么,需要尝试调用什么方法了。只要两个线程没有执行完,则主线程会被阻塞,待子线程全部执行完毕之后便自动恢复执行。这样我们是不是就将业务线程与主线程的逻辑解耦了?
如何使用
CountDownLatch
的使用非常简单,这个类只有 300+ 行代码,只有 4 个 public 的方法,大家可以自己阅读下。使用时先通过构造方法声明需要执行的子线程数量 CountDownLatch(int count)
,主要使用的方法有两个 await()
和 countDown()
:
await()
,会阻塞当前线程,直到 latch「计数器」 的数量为 0 时才恢复countDown()
,每次调用 latch「计数器」 数量会减 1 ,当 latch 数量为 0 时会唤醒此前阻塞的线程
在需要等待其他线程 finish 的地方调用 await()
,在子线程执行完毕时调用 countDown()
,就可以了。
使用场景
CountDownLatch
的工作流程是当前线程等待,直到其他线程(任务)执行完毕之后才回复。所以,有两个应用场景比较典型:
- 大型 App 首页展示:App 首页需要展示多个服务内容, 如广告、商品、电影等等,它们不太可能在同一个服务里,所以需要有多个异步请求,在所有请求处理完成之后再展示。
- 初始化:在使用某些能力时,需要初始化多个服务,在都初始化完成后再继续业务处理。
CyclicBarrier
字面意思循环栅栏或叫循环屏障,通过它可以实现让一组线程等待至某个状态之后再全部同时执行 。叫做循环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
主要方法:
java
public CyclicBarrier(int parties, Runnable barrierAction)
另外还有个单参数的构造函数,就相当于此处的 barrierAction
为空,这里就不另外列了。parties
就是需要让多少个线程(任务)等待至 barrier 状态。barrierAction
是在所有线程都到达 barrier 状态后,会优先执行「在被阻塞的线程之前」的内容。
java
public int await()
此方法用来挂起当前线程,直至所有线程都到达barrier状态再同时恢复执行后续任务
应用场景
比如,有一款游戏,就类似 PUBG 吧,可以创建房间,在房间达到四人且准备后开始游戏。或者跑步吧,100米比赛赛场有八个赛道,需要等待每个赛道的运动员都准备好才能开始比赛。我们来用 CyclicBarrier 实现这样的需求。
java
public static void main(String[] args) {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(4,()->{
System.out.println("所有玩家准备完毕,可以开始游戏了,开始前准备!");
});
for (int i = 0; i < 4; i++) {
new Thread("玩家「"+i+"」"){
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "正在准备");
Thread.sleep((long) (1000 * Math.random()));
System.out.println(Thread.currentThread().getName() + "准备好了");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "开始玩!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}.start();
}
}
============================================================================
玩家「0」正在准备
玩家「3」正在准备
玩家「1」正在准备
玩家「2」正在准备
玩家「1」准备好了
玩家「3」准备好了
玩家「2」准备好了
玩家「0」准备好了
所有玩家准备完毕,可以开始游戏了,开始前准备!
玩家「0」开始玩!
玩家「1」开始玩!
玩家「3」开始玩!
玩家「2」开始玩!
await()
就相当于玩家在游戏界面内点了「准备 」按钮,表示已经准备好了「即到达Barrier」。任务线程不知道其他线程状态,只能告知 CyclicBarrier「房主」我已经准备好了。当所有人都准备好了后,房主便可以先完成游戏开始前的准备工作再通知所有玩家,可以开始游戏了。
CountDownLatch 与 CyclicBarrier 的区别
- 它们都是用来控制一个或多个线程与一组线程之间的同步工具
- CountDownLatch 的状态无法重置,它的计数器在被归0后无法再恢复。CyclicBarrier 则提供了
reset
方法去重置状态,它可以被重复使用。 - 应用场景不太一样,CountDownLatch 是在计数器归 0 之后去恢复之前被阻塞的线程,只恢复这一个。而 CycliBarrier 是在
parties
线程数都到达栅栏「调用await()」后去恢复所有此前被阻塞的线程。恢复的线程数量是parties
个。
最后
关于CounDownLatch
、CyclicBarrier
的使用就跟大家聊这些,希望对大家会有帮助,也欢迎大家多多交流,谢谢!