CopyOnWriteArrayList
JUC下的一个集合,线程安全,写时复制,当往一个容器中添加元素时,先对容器进行复制,添加到复制的容器中,再将原容器的引用指向新容器
CountDownLatch
是JUC(java.util.concurrent)包中的一个同步工具类,可以把它理解成一个倒计数门闩。它让一个或多个线程在门闩前等待,直到其他线程完成一系列操作,把计数器减到0,这个门才会打开,等待的线程才能继续执行
CountDownLatch是处理线程协作的得力工具,尤其适合分而治之的任务场景,能有效简化多线程之间的协调逻辑
主要特点
- 计数器不可重置:计数器一旦减到0,就无法再重置了,是一次性的。如果需要循环使用,可以考虑CyclicBarrier(循环栅栏)
- 基于AQS实现:它的底层是基于AbstractQueuedSynchronizer(AQS,抽象队列同步器)框架实现的,通过一个共享锁的状态(state)来控制
两大核心方法
- await()方法:调用这个方法的线程会一直阻塞,直到计数器的值变为0
- countDown()方法:调用这个方法会将计数器减1。需要注意的是,调用countDown()的线程不会被阻塞。当计数器归零时,它会唤醒所有在await()上等待的线程
常见应用场景
主线程等待多个子任务完成:这是最常见的用法。主线程调用await(),然后启动多个子线程去执行任务,每个子线程执行完毕后调用countDown()。当所有子任务都完成,主线程才继续往下走
// 例子:主线程等待所有子线程完成任务
CountDownLatch doneSignal = new CountDownLatch(N);
ExecutorService e = Executors.newFixedThreadPool(N);
for (int i = 0; i < N; ++i) {
e.execute(() -> {
doWork(); // 执行子任务
doneSignal.countDown(); // 完成,计数减1
});
}
doneSignal.await(); // 主线程等待
System.out.println("所有任务完成");
多个线程同时开始执行:实现“统一发令,同时起跑”的效果。初始化一个计数器为1的CountDownLatch,所有工作线程先调用await()等待,主线程在准备好后调用countDown(),所有等待的线程就会同时被唤醒执行
CountDownLatch startSignal = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
startSignal.await(); // 等待发令枪
doWork(); // 同时开始执行
}).start();
}
// 主线程做准备工作...
startSignal.countDown(); // 发令,所有线程启动
最大并行性:利用两个CountDownLatch可以协调一组工作线程,第一个信号控制“同时开始”,第二个信号控制“一起结束”,这在压力测试中很常见
CyclicBarrier
CyclicBarrier(循环栅栏)和CountDownLatch很像,但它的核心概念是可循环使用的屏障
核心特点
- 计数器可重置:这是它与CountDownLatch最本质的区别。当所有等待的线程被释放后,计数器会自动重置为初始值,可以重复使用
- 支持聚齐动作:你可以指定一个Runnable任务,当最后一个线程到达屏障时,会由这个线程优先执行该任务(比如人齐了,先拍个合照),然后再唤醒所有等待的线程
常见应用场景
- 多线程分段计算(MapReduce思想):将一个大任务拆分成多个子任务,每个子任务由不同的线程计算。所有线程都计算完成后,在屏障点汇总结果,然后继续下一轮计算(如果循环)
- 模拟高并发测试:和CountDownLatch一样,也可以让多个线程同时开始执行,但CyclicBarrier可以在一轮测试结束后,自动重置,重复进行多轮压测
- 多阶段任务协同:比如一个复杂的游戏加载流程:所有玩家都加载完资源(第一阶段屏障) -> 都读完进度条(第二阶段屏障) -> 一起进入游戏
代码示例:3人斗地主准备
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 3个玩家准备,人齐后由最后一个到达的玩家执行“发牌”动作
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("人都到齐了,开始发牌!");
});
// 模拟3个玩家线程
for (int i = 1; i <= 3; i++) {
final int playerId = i;
new Thread(() -> {
try {
System.out.println("玩家" + playerId + " 已进入房间,等待其他人...");
barrier.await(); // 等待其他玩家
System.out.println("玩家" + playerId + " 开始打牌了!");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
输出结果(每次运行顺序可能不同,但逻辑一致):
玩家1 已进入房间,等待其他人...
玩家2 已进入房间,等待其他人...
玩家3 已进入房间,等待其他人...
人都到齐了,开始发牌!
玩家3 开始打牌了!
玩家1 开始打牌了!
玩家2 开始打牌了!
进阶提示:常见陷阱
- 线程数必须匹配:parties的值必须等于调用await()的线程数。如果线程数不够,所有线程会一直阻塞(类似“少一个人永远不开饭”),可能导致死锁
- 注意中断与重置:如果某个等待的线程被中断或超时,屏障会进入“破损”(Broken)状态,其他线程调用await()会抛出BrokenBarrierException。此时通常需要调用reset()来手动恢复
Semaphore
Semaphore(信号量)是JUC包中用于流量控制的同步工具,你可以把它想象成一个限流器
核心概念
- acquire():获取许可
- release():释放许可
Semaphore能确保最多只有N个线程可以同时访问某个资源或执行某段代码
常见应用场景
- 限流(控制并发数):最经典的用法。例如,数据库连接池最大连接数为10,用Semaphore(10)可以限制同时执行的数据库操作数量,防止数据库因连接过多而崩溃
- 资源池管理:管理有限的资源,比如线程池、对象池等。线程在使用资源前获取许可,用完后归还
- 实现“有界阻塞容器”:比如用两个信号量(一个控制添加,一个控制移除)来实现一个固定大小的阻塞队列
代码示例:限流器
下面模拟一个简单的限流场景,限制同时只有最多 3 个线程能执行打印任务:
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadLocalRandom;
public class SemaphoreDemo {
// 最多允许 3 个线程同时执行
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
// 模拟 10 个请求
for (int i = 0; i < 10; i++) {
final int taskId = i;
new Thread(() -> {
try {
semaphore.acquire(); // 1. 获取许可(占用一个车位)
System.out.println("任务 " + taskId + " 开始执行,当前并发数:"
+ (3 - semaphore.availablePermits()));
// 模拟任务执行耗时
Thread.sleep(ThreadLocalRandom.current().nextLong(1000, 3000));
System.out.println("任务 " + taskId + " 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 2. 释放许可(腾出一个车位)
}
}).start();
}
}
}
运行效果(控制台会显示,任意时刻最多只有3个任务在同时执行):
任务 0 开始执行,当前并发数:1
任务 1 开始执行,当前并发数:2
任务 2 开始执行,当前并发数:3
// 任务1执行完毕后...
任务 3 开始执行,当前并发数:3
// 任务0执行完毕后...
任务 4 开始执行,当前并发数:3
...