多线程详解-CountDownLatch与CyclicBarrier
前言
本文介绍CountDownLatch与CyclicBarrier,以及其中容易发生的问题。
一、CountDownLatch
介绍
CountDownLatch(倒计时门栓)是Java并发包中的一个工具类,用于在多线程编程中实现线程间的等待。它允许一个或多个线程等待其他线程完成操作后再继续执行。
用法详解
初始化CountDownLatch
CountDownLatch latch = new CountDownLatch(N);
其中,N 是需要等待完成的操作数量。当 N 个操作完成后,通过 countDown() 方法递减计数器,latch 的 await() 方法将会返回。
主要方法
countDown()
该方法使计数器减 1。当某个线程完成了自己的任务,调用此方法通知计数器减 1。
await()
调用此方法会使当前线程等待,直到计数器的值变为 0。一旦计数器为 0,等待的线程会继续执行。
示例详情
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
final int N = 3;
CountDownLatch latch = new CountDownLatch(N);
// 创建 N 个线程执行任务
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i, latch));
thread.start();
}
// 等待所有线程执行完毕
latch.await();
System.out.println("所有线程已执行完成,开始执行后续逻辑业务");
}
}
class Worker implements Runnable {
private final int id;
private final CountDownLatch latch;
public Worker(int id, CountDownLatch latch) {
this.id = id;
this.latch = latch;
}
@Override
public void run() {
System.out.println("线程" + id + "开始执行 ");
try {
Thread.sleep(2000); // 模拟任务耗时执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + id + " 执行完毕 ");
latch.countDown(); // 完成任务,计数器减 1
}
}
使用场景
1.主线程等待多个子线程全部执行完成后再继续执行。
2.控制并发线程数量,限制同时执行的线程数。
CountDownLatch 是一个非常有用的工具类,在并发编程中常被使用,特别是在多线程协作的场景中。
注意事项
虽然 CountDownLatch 是一个强大且有用的工具,但在使用时可能会遇到一些问题,特别是在不正确使用或不适当处理异常的情况下。以下是一些可能出现的问题:
1.未正确处理异常: 在使用 CountDownLatch 时,如果不正确处理线程可能抛出的异常,可能会导致某些线程提前退出而无法执行 countDown() 方法,进而导致主线程一直等待,无法继续执行。
2.死锁: 如果计数器的值不能达到零,主线程会一直等待,这可能会导致死锁。这可能是因为某些线程没有正确调用 countDown() 方法,或者计数器的值设置不正确。
3.计数器过早到达零: 如果在主线程调用 await() 方法之前,已经有线程调用了足够数量的 countDown() 方法,那么主线程将不会等待,可能导致在预期之前继续执行。
4.性能问题: 在高并发环境中,如果使用 CountDownLatch 进行大量的线程同步,可能会导致性能问题,因为所有线程都需要等待计数器归零才能继续执行。
5.不恰当的设计: 在某些情况下,可能会更适合使用其他同步工具,如 CyclicBarrier 或 Semaphore,而不是 CountDownLatch。如果选择了不合适的同步机制,可能会导致设计不够灵活或者性能不佳。
为避免这些问题,应该仔细设计并正确使用 CountDownLatch,并确保在适当的时候处理异常情况,以及在主线程和子线程之间进行合适的同步。
二、CyclicBarrier
介绍
CyclicBarrier(循环屏障)是 Java 并发包中的一个同步工具类,用于实现多个线程之间的同步。它允许一组线程相互等待,直到所有线程都达到某个共同的屏障点,然后继续执行。
用法详解
初始化CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(int parties, Runnable barrierAction);
parties 是参与线程的数量,即需要等待的线程数量。
barrierAction 是当所有线程到达屏障点时执行的操作。可以为 null。
主要方法
await()
当线程到达屏障点时调用,导致该线程等待,直到所有参与线程都到达屏障点。
reset()
将屏障重置为初始状态,用于重复使用。
示例详情
考虑一个场景,有一群游客参加一项活动,需要等待所有游客到齐后才能开始游戏。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
final int numPlayers = 5;
Runnable gameStartAction = () -> System.out.println("所有玩家已到齐,游戏开始!");
CyclicBarrier barrier = new CyclicBarrier(numPlayers, gameStartAction);
for (int i = 0; i < numPlayers; i++) {
Thread playerThread = new Thread(new Player(i, barrier));
playerThread.start();
}
}
}
class Player implements Runnable {
private final int playerId;
private final CyclicBarrier barrier;
public Player(int playerId, CyclicBarrier barrier) {
this.playerId = playerId;
this.barrier = barrier;
}
@Override
public void run() {
System.out.println("玩家 " + playerId + " 已到达...");
try {
barrier.await(); // 等待所有玩家到齐
System.out.println("玩家 " + playerId + " 开始游戏!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
使用场景
1.并行任务分解:将任务分解为多个子任务,并让多个线程并行执行这些子任务,最后再将结果合并。
2.控制并发阶段:在多个阶段的任务中,每个阶段需要等待所有线程完成后才能继续执行。
3.数据计算并行化:当数据处理任务可以被分解为多个独立的子任务时,可以使用 CyclicBarrier 来同步这些子任务的执行。
CyclicBarrier 提供了一种简单且强大的同步机制,能够有效地控制多个线程之间的协作。
注意事项
虽然 CyclicBarrier 是一个有用的同步工具,但在使用时可能会遇到一些问题。以下是一些可能出现的问题:
1.死锁: 如果在线程到达屏障点之前发生异常,并且没有正确处理,可能会导致线程永远等待,从而引发死锁。
2.线程数量不匹配: 如果在初始化 CyclicBarrier 时指定的参与线程数量与实际线程数量不匹配,可能会导致一些线程永远等待,或者在一些线程已经到达屏障点时其他线程仍在等待。
3.超时问题: CyclicBarrier 不支持超时,如果线程因某种原因无法到达屏障点,其他线程将永远等待。如果需要超时功能,可以考虑使用 CountDownLatch 或 Semaphore。
4.内存泄漏: 如果在使用完毕后没有显式调用 reset() 方法重置 CyclicBarrier,可能会导致内存泄漏问题,因为 CyclicBarrier 内部会持有对线程的引用。
5.屏障动作异常: 如果在初始化 CyclicBarrier 时指定了屏障动作,并且该动作抛出了异常,可能会导致其他线程无法正常执行。
为避免这些问题,应该在使用 CyclicBarrier 时仔细考虑可能出现的异常情况,并且确保正确处理。特别是,在多线程环境中,应该正确处理线程可能抛出的异常,以避免导致程序不稳定或者死锁等问题的发生。
三、CountDownLatch与CyclicBarrier区别
- 使用次数
CyclicBarrier 可以重用,当一组线程到达屏障点后,它会重置并等待下一次使用。
CountDownLatch 只能使用一次,一旦计数器归零,就无法重置或再次使用。 - 等待条件
CyclicBarrier 用于一组线程彼此等待,直到所有线程都到达一个共同的屏障点,然后继续执行。
CountDownLatch 用于一个或多个线程等待另一组线程执行完特定操作后再继续执行。 - 计数器类型
CyclicBarrier 内部的计数器是递增的,每个线程调用 await() 方法时计数器加一,直到计数器达到预设的值时释放所有等待线程。
CountDownLatch 内部的计数器是递减的,初始化时设置一个初始值,每次调用 countDown() 方法计数器减一,直到计数器为零时释放所有等待线程。 - 屏障点动作
CyclicBarrier 可以在所有线程到达屏障点时执行一个可选的屏障动作。
CountDownLatch 没有屏障动作。 - 重置能力
CyclicBarrier 可以通过 reset() 方法重置屏障,以便在之后的使用中重新初始化。
CountDownLatch 不能重置,一旦计数器归零,就无法再次使用。 - 异常处理
CyclicBarrier 的等待线程可以抛出 BrokenBarrierException 异常,表示某个等待线程在等待过程中被中断或者超时。
CountDownLatch 的等待线程无法抛出异常。
总的来说,CyclicBarrier 适用于一组线程相互等待,然后同时执行某个操作,而 CountDownLatch 适用于一个或多个线程等待其他一组线程完成某个操作后继续执行。