2026年7月11日 · 5 分钟阅读
Java 并发工具实战:线程池、并发容器、阻塞队列、ThreadLocal 与 Future
梳理 Java 并发常用工具,包括 CountDownLatch、Semaphore、CyclicBarrier、Condition、并发容器、阻塞队列、线程池、ThreadLocal 和 Future。
理解了 JMM、锁和 AQS 之后,再看 JUC 工具类就轻松很多。它们解决的不是“能不能加锁”,而是更具体的协作问题:等一组任务完成、限制并发数、线程间通信、管理任务队列、复用线程。
这篇把常用并发工具放在一条工程主线上。
CountDownLatch
CountDownLatch 可以理解为倒数门闩。一个或多个线程等待计数器归零,其他线程完成任务后调用 countDown()。
典型场景:
- 主线程等待多个子任务完成。
- 服务启动时等待多个组件初始化完成。
- 压测时让多个线程同时开始执行。
示例:
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// do work
} finally {
latch.countDown();
}
}).start();
}
latch.await();
CountDownLatch 不能重置,计数归零后就结束了。
Semaphore
Semaphore 是信号量,用来控制同时访问某个资源的线程数量。
典型场景:
- 限制同时访问数据库连接的线程数。
- 限制接口并发调用数量。
- 控制某类资源的许可证数量。
示例:
Semaphore semaphore = new Semaphore(10);
semaphore.acquire();
try {
// access limited resource
} finally {
semaphore.release();
}
它关注的是“最多允许多少线程同时进入”。
CyclicBarrier
CyclicBarrier 是循环栅栏,用来让一组线程互相等待,直到所有线程都到达某个屏障点,再一起继续执行。
它和 CountDownLatch 的区别:
| 工具 | 特点 |
|---|---|
| CountDownLatch | 等待事件完成,计数不能重置 |
| CyclicBarrier | 等待一组线程到齐,可以重复使用 |
CyclicBarrier 适合分阶段并行任务,比如多个线程完成第一阶段后,再一起进入第二阶段。
Condition
Condition 是和 Lock 配合使用的条件对象,可以替代 Object.wait/notify。
它的优势是一个 Lock 可以创建多个 Condition,让不同等待条件分开管理。
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
阻塞队列的实现里,经常会看到类似的两个条件:队列非空、队列未满。
并发容器
普通集合在多线程下不一定安全。JUC 提供了多种并发容器。
| 容器 | 适用场景 |
|---|---|
ConcurrentHashMap | 高并发 Map |
CopyOnWriteArrayList | 读多写少的 List |
ConcurrentLinkedQueue | 非阻塞队列 |
BlockingQueue | 生产者消费者 |
ConcurrentHashMap 适合高并发读写。CopyOnWriteArrayList 写入时复制数组,读操作无锁,适合读多写少,比如配置列表、监听器列表。
阻塞队列
阻塞队列是线程池和生产者消费者模型的核心组件。
常见阻塞队列包括:
| 队列 | 特点 |
|---|---|
ArrayBlockingQueue | 基于数组的有界阻塞队列 |
LinkedBlockingQueue | 基于链表的阻塞队列,可有界也可近似无界 |
SynchronousQueue | 不存储元素,直接移交任务 |
PriorityBlockingQueue | 支持优先级的无界阻塞队列 |
DelayQueue | 延迟队列 |
LinkedTransferQueue | 支持传输语义的无界队列 |
LinkedBlockingDeque | 双端阻塞队列 |
队列的选择会直接影响线程池行为。
为什么需要线程池
直接创建线程有几个问题:
- 创建和销毁线程成本高。
- 线程数量不可控。
- 频繁上下文切换影响性能。
- 缺少统一监控和拒绝策略。
线程池用池化思想复用线程,把线程创建、任务排队、任务执行、拒绝策略统一管理起来。
ThreadPoolExecutor 参数
ThreadPoolExecutor 核心参数包括:
| 参数 | 含义 |
|---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 非核心线程空闲存活时间 |
workQueue | 工作队列 |
threadFactory | 线程创建工厂 |
handler | 拒绝策略 |
线程池执行流程可以简化为:
- 当前线程数小于核心线程数,直接创建核心线程执行任务。
- 核心线程满了,任务进入队列。
- 队列满了,且线程数小于最大线程数,创建非核心线程。
- 线程数达到最大值且队列也满了,执行拒绝策略。
不建议随便用 Executors
Executors 提供了很多快捷工厂方法,但容易隐藏风险。
例如:
newFixedThreadPool默认使用无界队列,可能堆积大量任务。newCachedThreadPool最大线程数非常大,可能创建过多线程。newSingleThreadExecutor也可能堆积任务。
更推荐手动创建 ThreadPoolExecutor,明确线程数、队列大小、线程名称和拒绝策略。
ThreadLocal
ThreadLocal 用于给每个线程保存一份独立变量副本。
典型场景:
- 保存用户上下文。
- 保存链路追踪 ID。
- 保存数据库连接或事务上下文。
- 在跨方法调用中传递线程级数据。
使用时要注意在线程池中及时清理:
try {
context.set(value);
// do work
} finally {
context.remove();
}
线程池会复用线程,如果不 remove(),旧请求的数据可能污染新请求,也可能造成内存泄漏。
ThreadLocal 为什么用弱引用
ThreadLocalMap 的 Entry 中,Key 是 ThreadLocal 的弱引用。这样当外部不再引用某个 ThreadLocal 时,Key 可以被 GC 回收。
但 Value 仍然可能留在当前线程的 ThreadLocalMap 中。所以弱引用不能完全避免内存泄漏,正确做法仍然是使用完后调用 remove()。
Future 和 FutureTask
Future 表示一个异步任务的结果。
常见方法:
| 方法 | 作用 |
|---|---|
get() | 等待并获取结果 |
get(timeout) | 限时等待结果 |
cancel() | 尝试取消任务 |
isDone() | 判断任务是否完成 |
isCancelled() | 判断任务是否取消 |
线程池的 submit() 方法会返回 Future:
Future<String> future = executor.submit(() -> "ok");
String result = future.get();
FutureTask 同时实现了 Runnable 和 Future,既可以作为任务执行,也可以获取执行结果。
小结
JUC 工具可以按问题分类:
- 等待多个任务完成:
CountDownLatch。 - 限制并发数量:
Semaphore。 - 多线程阶段协作:
CyclicBarrier。 - 精细条件等待:
Condition。 - 线程安全集合:并发容器。
- 任务排队:阻塞队列。
- 线程复用:线程池。
- 线程级上下文:
ThreadLocal。 - 异步结果:
Future。
并发工具不是越多越好,关键是先说清楚协作模型,再选择最贴合的工具。