关键词:Java并发编程、CountDownLatch、CyclicBarrier、线程同步、并发工具类、多线程协作、同步屏障
摘要:在Java并发编程中,
CountDownLatch
和CyclicBarrier
是两个非常重要的同步工具类。它们就像多线程世界里的“协调员”,能帮助我们高效管理线程间的协作。本文将通过生活案例、代码实战和场景对比,用“给小学生讲故事”的方式,彻底讲透这两个工具的核心原理、区别和典型应用场景,帮助你在实际开发中快速选择合适的工具解决问题。
在多线程编程中,我们经常需要让多个线程“配合工作”:有的线程需要等待其他线程完成任务后才能开始,有的需要多个线程“步调一致”地推进阶段任务。CountDownLatch
和CyclicBarrier
就是专门解决这类问题的工具。本文将聚焦这两个工具的核心原理、使用场景和对比分析,覆盖从概念理解到代码实战的完整学习路径。
Thread
、Runnable
)的开发者;本文将按照“概念引入→原理讲解→代码实战→场景对比→总结”的逻辑展开:
CountDownLatch
、CyclicBarrier
);CountDownLatch
中的一个整数变量,初始值为需要等待的线程数,每完成一个线程任务就减1;CyclicBarrier
中的“同步点”,所有线程必须到达该点后才能继续执行;CyclicBarrier
在完成一次屏障后可以重置,再次用于新的一轮线程协作(而CountDownLatch
不可重置)。假设我们组织了一场登山活动,有两种不同的协作场景:
场景1:家长会式等待(CountDownLatch)
老师要开家长会,但需要等所有家长(3位)到齐才能开始。这时老师(主线程)会在教室门口等待,每到一位家长(子线程完成任务),就“计数减1”,直到3位家长都到齐(计数器归零),老师才开始开会(主线程继续执行)。
场景2:团队登山式协作(CyclicBarrier)
登山队有3名队员,他们约定在“半山腰凉亭”(第一阶段屏障)、“山顶观景台”(第二阶段屏障)集合。每次到达集合点时,队员们必须等待所有人到齐(触发屏障),才能一起继续出发(进入下一阶段)。如果有人迟到,其他人会一直等,直到最后一人到达。
这两个场景完美对应了CountDownLatch
和CyclicBarrier
的核心功能:前者是“一个线程等多个线程完成”,后者是“多个线程互相等待,一起推进阶段任务”。
想象你有一个“倒计时计数器”,初始值设为3(比如需要等3个任务完成)。当每个任务完成时,计数器减1(就像拔下一根门闩)。当计数器变成0时,所有被“门闩”挡住的线程就可以通过了。
生活类比:过年放烟花时,需要同时点燃3根引线。你在旁边等待,每点燃一根引线(子线程完成任务),就数“1、2、3”,数到3时(计数器归零),烟花就会一起炸开(等待的线程继续执行)。
想象有一个“魔法门”,上面有一个计数器,初始值设为3(需要3个人一起推门)。当第一个人到达门前,计数器减1(变成2),但门不会开;第二个人到达,计数器减1(变成1),门还是不开;第三个人到达,计数器减到0,门“轰”地打开,三个人一起通过。更神奇的是,门打开后会自动重置计数器(回到3),下次还能继续用!
生活类比:小朋友玩“跳房子”游戏,3个小伙伴约好“数到3一起跳”。第一个喊“1”,第二个喊“2”,第三个喊“3”,这时三个人一起跳出去。跳完后,他们又可以重新站好,再次喊“1、2、3”一起跳(屏障循环使用)。
CountDownLatch
和CyclicBarrier
都是多线程协作的“协调员”,但它们的“工作方式”完全不同:
CountDownLatch:像“单向门”——一旦计数器归零,门就永远打开,不能再关闭(不可重复使用);
适合场景:主线程等待所有子任务完成(如等待数据加载完毕)。
CyclicBarrier:像“旋转门”——每次所有人通过后,门会重置,等待下一批人(可重复使用);
适合场景:多个线程分阶段协作(如分阶段计算、多步骤任务同步)。
关系总结:
两者都解决“线程等待”问题,但CountDownLatch
是“单向等待”,CyclicBarrier
是“循环协作”。就像运动会的“接力赛”(CountDownLatch:最后一棒冲线后比赛结束)和“团体跳绳”(CyclicBarrier:每次跳完10个后重新开始计数)。
volatile
修饰的计数器(基于AQS,AbstractQueuedSynchronizer);countDown()
:子线程完成任务后调用,计数器减1;await()
:主线程调用,阻塞直到计数器归零。parties
(需要等待的线程数)和count
(当前剩余等待线程数);await()
:每个线程到达屏障时调用,count
减1;若count
归零,触发所有线程继续执行,并重置count
为parties
。graph TD
A[主线程启动3个子线程] --> B[子线程1完成任务,调用countDown()]
A --> C[子线程2完成任务,调用countDown()]
A --> D[子线程3完成任务,调用countDown()]
B --> E[计数器从3→2]
C --> F[计数器从2→1]
D --> G[计数器从1→0]
G --> H[主线程的await()被唤醒,继续执行]
graph TD
A[线程1进入阶段1,调用await()] --> B[count=2]
C[线程2进入阶段1,调用await()] --> D[count=1]
E[线程3进入阶段1,调用await()] --> F[count=0]
F --> G[所有线程通过阶段1,进入阶段2]
G --> H[线程1进入阶段2,调用await()]
G --> I[线程2进入阶段2,调用await()]
G --> J[线程3进入阶段2,调用await()]
J --> K[count=0,所有线程通过阶段2,任务完成]
CountDownLatch
的核心是一个继承自AbstractQueuedSynchronizer
(AQS)的内部类Sync
。AQS是Java并发包的“基石”,通过维护一个state
变量(在这里就是计数器)和一个等待队列实现同步。
count
,state
被初始化为count
;state
减1(CAS操作保证原子性),若减到0,唤醒等待队列中的线程;state
是否为0,若不为0则将当前线程加入等待队列并阻塞。CyclicBarrier
内部使用ReentrantLock
和Condition
实现同步:
parties
(需要等待的线程数)和一个可选的Runnable
(所有线程到达屏障后执行的回调);count
减1(count
初始为parties
);count
不为0,调用Condition.await()
阻塞当前线程;count
为0,调用回调(如果有),并通过Condition.signalAll()
唤醒所有等待线程,然后重置count
为parties
(循环使用)。设初始计数器为 N N N,每个子线程完成任务后计数器减1,当计数器 N ′ = 0 N'=0 N′=0时,等待线程被唤醒。数学上可以表示为:
N ′ = N − k ( k 为已完成任务的线程数 ) N' = N - k \quad (k \text{为已完成任务的线程数}) N′=N−k(k为已完成任务的线程数)
当 N ′ = 0 N'=0 N′=0时,触发唤醒条件。
举例: N = 3 N=3 N=3,3个子线程各调用一次countDown()
,则 k = 3 k=3 k=3, N ′ = 3 − 3 = 0 N'=3-3=0 N′=3−3=0,主线程被唤醒。
设需要等待的线程数为 P P P(parties
),当前剩余等待线程数为 C C C(count
)。每个线程调用await()
后, C C C减1:
C ′ = C − 1 C' = C - 1 C′=C−1
当 C ′ = 0 C'=0 C′=0时,所有线程被唤醒, C C C重置为 P P P,进入下一轮。
举例: P = 3 P=3 P=3,线程1调用await()
后 C = 2 C=2 C=2,线程2调用后 C = 1 C=1 C=1,线程3调用后 C = 0 C=0 C=0,触发唤醒,然后 C C C重置为3,可用于下一轮协作。
ConcurrentDemo
类,包含CountDownLatchDemo
和CyclicBarrierDemo
两个内部类。假设我们要开发一个电商系统,启动时需要加载3类数据:商品数据、用户数据、订单数据。主线程必须等待这3类数据加载完成后,才能输出“系统启动成功”。
import java.util.concurrent.CountDownLatch;
public class ConcurrentDemo {
static class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化CountDownLatch,计数器为3(需要等待3个任务)
CountDownLatch latch = new CountDownLatch(3);
// 启动3个数据加载线程
new Thread(() -> {
loadData("商品数据");
latch.countDown(); // 任务完成,计数器减1
}, "商品线程").start();
new Thread(() -> {
loadData("用户数据");
latch.countDown();
}, "用户线程").start();
new Thread(() -> {
loadData("订单数据");
latch.countDown();
}, "订单线程").start();
// 主线程等待,直到计数器归零
System.out.println("主线程:等待数据加载...");
latch.await();
// 所有数据加载完成,系统启动
System.out.println("主线程:所有数据加载完成,系统启动成功!");
}
private static void loadData(String dataType) {
try {
System.out.println(Thread.currentThread().getName() + ":开始加载" + dataType);
Thread.sleep(1000); // 模拟加载耗时
System.out.println(Thread.currentThread().getName() + ":" + dataType + "加载完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
}
CountDownLatch
:new CountDownLatch(3)
表示需要等待3个任务完成;Thread.sleep(1000)
),完成后调用latch.countDown()
(计数器减1);latch.await()
会阻塞主线程,直到计数器归零;运行结果示例:
商品线程:开始加载商品数据
用户线程:开始加载用户数据
订单线程:开始加载订单数据
主线程:等待数据加载...
商品线程:商品数据加载完成
用户线程:用户数据加载完成
订单线程:订单数据加载完成
主线程:所有数据加载完成,系统启动成功!
假设我们要开发一个分布式计算系统,需要3个计算节点协作完成两阶段计算:第一阶段计算原始数据,第二阶段汇总结果。每个节点必须完成当前阶段任务后,等待其他节点一起进入下一阶段。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class ConcurrentDemo {
static class CyclicBarrierDemo {
public static void main(String[] args) {
// 初始化CyclicBarrier,等待3个线程,所有线程到达后执行回调
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程完成当前阶段,进入下一阶段!");
});
// 启动3个计算线程
for (int i = 0; i < 3; i++) {
new Thread(new Calculator(barrier, i), "计算节点" + (i + 1)).start();
}
}
static class Calculator implements Runnable {
private final CyclicBarrier barrier;
private final int nodeId;
public Calculator(CyclicBarrier barrier, int nodeId) {
this.barrier = barrier;
this.nodeId = nodeId;
}
@Override
public void run() {
try {
// 阶段1:计算原始数据
System.out.println(Thread.currentThread().getName() + ":开始阶段1计算");
Thread.sleep(1000); // 模拟计算耗时
System.out.println(Thread.currentThread().getName() + ":阶段1计算完成");
barrier.await(); // 等待其他节点到达阶段1屏障
// 阶段2:汇总结果
System.out.println(Thread.currentThread().getName() + ":开始阶段2汇总");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":阶段2汇总完成");
barrier.await(); // 等待其他节点到达阶段2屏障
System.out.println(Thread.currentThread().getName() + ":所有阶段完成!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
}
CyclicBarrier
:new CyclicBarrier(3, 回调)
表示需要等待3个线程,所有线程到达屏障后执行回调(输出“进入下一阶段”);barrier.await()
:
await()
,线程阻塞直到3个线程都到达;await()
,线程阻塞直到3个线程都到达;运行结果示例:
计算节点1:开始阶段1计算
计算节点2:开始阶段1计算
计算节点3:开始阶段1计算
计算节点1:阶段1计算完成
计算节点2:阶段1计算完成
计算节点3:阶段1计算完成
所有线程完成当前阶段,进入下一阶段!
计算节点1:开始阶段2汇总
计算节点2:开始阶段2汇总
计算节点3:开始阶段2汇总
计算节点1:阶段2汇总完成
计算节点2:阶段2汇总完成
计算节点3:阶段2汇总完成
所有线程完成当前阶段,进入下一阶段!
计算节点1:所有阶段完成!
计算节点2:所有阶段完成!
计算节点3:所有阶段完成!
await()
后统计总耗时)。随着分布式系统和微服务架构的普及,多线程协作的场景越来越复杂,对同步工具的要求也越来越高:
CountDownLatch
和CyclicBarrier
是JVM内的工具,分布式系统中需要类似的协调机制(如Redis的Redisson
提供分布式CountDownLatch
);CyclicBarrier
在await()
时若线程被中断会抛出BrokenBarrierException
,未来可能支持更友好的恢复机制;CountDownLatch
的AQS实现可能成为瓶颈,未来可能出现更轻量级的同步工具。CountDownLatch
是“单向等待”(不可重置),CyclicBarrier
是“循环协作”(可重置);CountDownLatch
,需要“多个阶段协作”用CyclicBarrier
。CountDownLatch
还是CyclicBarrier
?为什么?CyclicBarrier
实现“主线程等待子线程”,可能会遇到什么问题?(提示:CyclicBarrier
要求所有线程都调用await()
)CountDownLatch
的countDown()
可以在多个线程中调用吗?如果某个线程调用多次countDown()
,会发生什么?Q1:CountDownLatch
和CyclicBarrier
都能等待线程完成,为什么需要两个工具?
A:CountDownLatch
是“单向”的(计数器归零后无法复用),适合一次性等待场景;CyclicBarrier
是“循环”的(可重置),适合多阶段协作场景。例如,前者像“一次性门闩”,后者像“旋转门”。
Q2:CyclicBarrier
的await()
为什么会抛出BrokenBarrierException
?
A:当某个线程在await()
时被中断,或者屏障被重置(如reset()
方法),其他等待的线程会收到BrokenBarrierException
,表示屏障已损坏,无法继续等待。
Q3:CountDownLatch
的计数器可以重置吗?
A:不能。CountDownLatch
的计数器一旦归零就无法重置,若需要重复使用,必须创建新的实例。而CyclicBarrier
通过reset()
方法可以重置计数器,重复使用。
CountDownLatch.java
和CyclicBarrier.java
的src
目录;