CyclicBarrier、CountDownLatch 和 Semaphore 是 Java 中用于多线程并发控制的工具类,它们的实现原理和适用场景各不相同,以下是对这三者的详细解释:
1. CyclicBarrier(循环屏障)
实现原理:
CyclicBarrier 主要用于让一组线程在某个屏障位置相互等待,直到所有线程都到达该屏障后,才能继续执行。它的实现原理涉及以下几点:
- 核心属性:
- 一个
parties参数,表示参与线程的总数。 - 一个
count参数,表示当前等待屏障的线程数量,初始值为parties。 - 一个
Runnable类型的任务(可选),可以在所有线程都到达屏障后由一个线程执行。
- 一个
- 同步控制:
CyclicBarrier通过ReentrantLock和Condition来管理线程的等待。当一个线程到达屏障时,会调用await()方法,count减 1,然后判断count是否为 0。如果不为 0,该线程会阻塞,进入等待队列;如果为 0,表示所有线程都到达屏障,唤醒所有等待的线程,并执行预定义的任务(如果有)。
- 可重复使用:
CyclicBarrier在屏障被突破后会重置count,允许它被再次使用。相比于CountDownLatch只能使用一次,CyclicBarrier具有重用的特点,这就是“循环”名称的由来。
使用场景:
- 多线程的阶段性同步:比如并行处理某个任务时,多个线程需要同步到某个点后,再继续执行下一阶段。适用于多个线程之间的同步机制。
- 任务分片和合并:比如在分布式计算中,多个线程并行处理任务,最后合并处理结果,要求所有线程完成前一个阶段才能继续进入下一个阶段。
代码示例:
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程已到达屏障,执行任务");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障");
barrier.await(); // 等待其他线程到达
System.out.println(Thread.currentThread().getName() + " 开始执行后续任务");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
-
输出示例
:
Thread-0 到达屏障 Thread-1 到达屏障 Thread-2 到达屏障 所有线程已到达屏障,执行任务 Thread-0 开始执行后续任务 Thread-1 开始执行后续任务 Thread-2 开始执行后续任务
2. CountDownLatch(倒计时门)
实现原理:
CountDownLatch 允许一个或多个线程等待,直到其他线程完成某些操作。它通过一个计数器来实现,初始值是线程或任务的数量。每当一个线程完成任务,计数器就会递减,直到减为 0 时,所有等待线程将继续执行。
- 核心属性:
- 一个
count计数器,初始值是需要完成任务的线程数量。 - 每次调用
countDown()方法,count减 1;当count变为 0 时,阻塞的线程将被唤醒。
- 一个
- 同步控制:
CountDownLatch底层依赖AQS(AbstractQueuedSynchronizer)实现,利用ReentrantLock来进行线程间的同步和计数管理。调用await()方法的线程将进入等待状态,直到count变为 0。
- 不可重复使用:
CountDownLatch是一次性的,计数器一旦变为 0,无法重置或重复使用。
使用场景:
-
等待一组任务完成
:当多个线程执行任务时,主线程需要等待这些任务完成后再继续执行。
- 比如主线程需要等待多个子线程的初始化工作完成后再继续。
-
启动准备工作:多个线程都准备好后,才统一启动某个操作,比如在并发测试中,等待所有线程准备好后统一开始测试。
代码示例:
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 完成任务");
latch.countDown(); // 计数器减 1
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
try {
latch.await(); // 主线程等待,直到计数器为 0
System.out.println("所有任务完成,主线程继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
-
输出示例
:
Thread-0 完成任务 Thread-1 完成任务 Thread-2 完成任务 所有任务完成,主线程继续执行
3. Semaphore(信号量)
实现原理:
Semaphore 主要用于控制同时访问某个资源的线程数量。它维护一个许可证(permit)的计数器,每个线程在进入时需要获得许可证,完成任务后必须释放许可证。
- 核心属性:
- 一个计数器
permits,表示可以同时访问资源的最大线程数量。 - 当线程请求许可证时,如果
permits大于 0,则直接获取并继续执行;如果permits为 0,线程将被阻塞,直到有其他线程释放许可证。
- 一个计数器
- 同步控制:
Semaphore基于AQS实现,它通过内部队列来控制线程的阻塞和许可的获取与释放。线程调用acquire()时,如果计数器大于 0,许可证就会发放,计数器减 1;调用release()则是将计数器加 1,并可能唤醒等待的线程。
使用场景:
-
限流控制
:用于限制某个资源的并发访问数量,防止资源过载。
- 比如限制对数据库连接池的并发访问数量、限制对 API 的并发请求数。
-
流量调控:控制多线程对共享资源的访问速率。
代码示例:
Semaphore semaphore = new Semaphore(3); // 最大并发线程数为 3
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 获得许可,开始执行");
Thread.sleep(2000); // 模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放许可");
semaphore.release(); // 释放许可
}
}).start();
}
-
输出示例
:
Thread-0 获得许可,开始执行 Thread-1 获得许可,开始执行 Thread-2 获得许可,开始执行 Thread-0 释放许可 Thread-3 获得许可,开始执行 ...
总结对比:
- CyclicBarrier 适合多线程协调的阶段性同步,所有线程需要同时达到某个状态才继续执行,且可以重用。
- CountDownLatch 适合一次性等待其他线程完成某些操作,倒计时计数器达到 0 后,阻塞的线程才会被唤醒。
- Semaphore 用于控制并发线程的数量,限制访问共享资源的线程数,适合限流或资源保护场景。