Java并发包(JUC)下有两个高频出现的工具类——CountDownLatch和CyclicBarrier。都是Doug Lea大神设计用于线程间同步的工具类。本篇是个人在学习这两个工具类的使用时,想起操作系统为了控制管程中只有一个线程在执行而定义了三种方法模型:Hansen(汉森)模型、Hoare(霍尔)模型、MESA(梅萨)模型。那么CountDownLatch和CyclicBarrier和这三种模型之间是否有什么联系?
Hansen模型的实现:CountDownLatch
Hansen模型:线程T1唤醒T2后,T1并不会阻塞也不一定让出CUP给T2。因此管程中就存在多个线程并行,所以编码时要求将T1唤醒T2的操作放到最后一步,这样才能保障管程内只有一个线程执行。
下面一段代码模拟的是没有将唤醒操作放在最后一步时,管程中活跃了2个线程(T1,T2)
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(()->{
countDownLatch.countDown(); // T1唤醒T2放在了第一步
Sleeper.sleep(2); // 模拟业务执行了2s
log.info("T1业务执行完毕!");
},"T1").start();
Thread.currentThread().setName("T2");
countDownLatch.await();
log.info("T2线程继续执行");
}
}
控制台输出:
c.TestCountDownLatch [T2] - T2线程继续执行
c.TestCountDownLatch [T1] - T1业务执行完毕!
可以看到调用.countDown()方法后,T2线程立马被唤醒,而此时T1线程进入了2s的睡眠后继续执行。
因此以上编码就不符合Hansen模型,正确的编码方式为:
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(()->{
Sleeper.sleep(2); // 模拟业务执行了2s
log.info("T1业务执行完毕!");
countDownLatch.countDown(); // 放在最后一步
},"T1").start();
Thread.currentThread().setName("T2");
countDownLatch.await();
log.info("T2线程继续执行");
}
}
控制台输出:
c.TestCountDownLatch [T1] - T1业务执行完毕!
c.TestCountDownLatch [T2] - T2线程继续执行
Hoare模型的实现:CyclicBarrier
Hoare模型:线程T1唤醒T2后,T1立马进入阻塞等待T2执行完后再唤醒T1继续执行。也能保证同一时刻只有一个线程执行。
其实严格意义上讲Java实现CyclicBarrier并不是按照Hoare模型定义的那样去执行的,但是同样有T1唤醒T2后,T1进入阻塞等待,T2执行完毕后T1才继续执行的效果。
Java的实现方式是线程T1调用了CyclicBarrier的.await()方法后,假如满足了CyclicBarrier等待的线程数则调用barrierAction定义的逻辑代码。
但是,执行barrierAction代码逻辑是T1线程本身去调用,而不是新开一个线程去调用barrierAction。
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(1, () -> {
Sleeper.sleep(2); // 模拟业务执行了2s
log.info("主业务执行完毕");
});
new Thread(() -> {
try {
cyclicBarrier.await();
log.info("开始执行T1业务");
} catch (Exception e) {
e.printStackTrace();
}
}, "T1").start();
}
}
控制台输出:
c.TestCyclicBarrier [T1] - 主业务执行完毕
c.TestCyclicBarrier [T1] - 开始执行T1业务
在CyclicBarrier构造函数中定义了barrierAction(构造参数中的Lambda表达式),当T1调用.await()方法时触发barrierAction的执行;从控制台输出的结果看出,T1调用.await()方法后相当于“阻塞”(其实不是阻塞),等待barrierAction中的逻辑执行完毕才继续执行。
这种T1调用.await()立马“阻塞”,等barrierAction执行完才继续执行的现象与Hoare模型达到的效果是一摸一样的。
另外多说一下MESA(梅萨)模型。synchronized的.wait()和.notify()采用的正是MESA模型,同样的,ReentrantLock实现线程同步的Condition条件变量也是MESA模型。
CountDownLatch、CyclicBarrier如何正确使用
上面解释CountDownLatch、CyclicBarrier对应的管程模型后,接下来介绍在实际开发中这两个工具类的该如何正确使用。首先先理解一点为什么需要线程同步?线程同步的作用无非就两点
控制共享资源,定义同时能操作共享资源的线程数量 梳理多个线程操作共享资源时的运行顺序
“九九归一” CountDownLatch
CountDownLatch最经典的用法就是多个子线程并行执行,执行完毕后在代码的最后一步执行.countDown()方法。
当CountDownLatch的count为0时触发主线程继续执行,即主线程等待所有的分散的子线程中的业务逻辑执行完再执行await后面的逻辑

“人满发车” CyclicBarrier
CyclicBarrier的使用思想可以这么认为:首先收集一波子线程,收集够了以后执行barrierAction逻辑,执行完barrierAction后再并发的执行收集到的子线程逻辑。有点“人满发车”的感觉,等所有子线程上车后,就运行barrierAction。
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
Sleeper.sleep(2); // 模拟业务执行了2s
log.info("主业务执行完毕");
});
// 线程1
new Thread(() -> {
try {
cyclicBarrier.await();
log.info("开始执行T1业务");
} catch (Exception e) {
e.printStackTrace();
}
}, "T1").start();
// 线程2
new Thread(() -> {
try {
cyclicBarrier.await();
log.info("开始执行T2业务");
} catch (Exception e) {
e.printStackTrace();
}
}, "T2").start();
}
}
控制台输出:
c.TestCyclicBarrier [T2] - 主业务执行完毕
c.TestCyclicBarrier [T2] - 开始执行T2业务
c.TestCyclicBarrier [T1] - 开始执行T1业务
在上面的代码中cyclicBarrier需要收集两个子线程才执行构造参数中的barrierAction。线程1、线程2调用.await();就表示上了cyclicBarrier的车。当线程2调用.await()时,cyclicBarrier发现所需收集的线程已满足,就会立刻执行barrierAction逻辑,barrierAction逻辑执行完毕后才开始执行线程1、线程2中.await()后面的逻辑。




