通俗易懂地聊 Java 线程同步(三)CountDownLatch、CyclicBarrier 如何使用

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个。

最后

关于CounDownLatchCyclicBarrier的使用就跟大家聊这些,希望对大家会有帮助,也欢迎大家多多交流,谢谢!

相关推荐
《源码好优多》几秒前
基于Java Springboot未央商城管理系统
java·开发语言·spring boot
^Lim5 分钟前
esp32 JTAG 串口 bootload升级
java·linux·网络
江-小北10 分钟前
Java基础面试题04:Iterator 和 ListIterator 的区别是什么?
java·开发语言
wmd1316430671213 分钟前
IDEA插件CamelCase,快速转变命名格式
java·ide·intellij-idea
捂月1 小时前
Spring Boot 核心逻辑与工作原理详解
java·spring boot·后端
埋头编程~1 小时前
【C++】踏上C++学习之旅(十):深入“类和对象“世界,掌握编程黄金法则(五)(最终篇,内含初始化列表、静态成员、友元以及内部类等等)
java·c++·学习
菜鸟起航ing1 小时前
Java中日志采集框架-JUL、Slf4j、Log4j、Logstash
java·开发语言·log4j·logback
Nightselfhurt1 小时前
RPC学习
java·spring boot·后端·spring·rpc
苹果醋31 小时前
vue3 在哪些方便做了性能提升?
java·运维·spring boot·mysql·nginx
孔汤姆1 小时前
部署实战(二)--修改jar中的文件并重新打包成jar文件
java·pycharm·jar