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拒绝策略

线程池执行流程可以简化为:

  1. 当前线程数小于核心线程数,直接创建核心线程执行任务。
  2. 核心线程满了,任务进入队列。
  3. 队列满了,且线程数小于最大线程数,创建非核心线程。
  4. 线程数达到最大值且队列也满了,执行拒绝策略。

不建议随便用 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 同时实现了 RunnableFuture,既可以作为任务执行,也可以获取执行结果。

小结

JUC 工具可以按问题分类:

  • 等待多个任务完成:CountDownLatch
  • 限制并发数量:Semaphore
  • 多线程阶段协作:CyclicBarrier
  • 精细条件等待:Condition
  • 线程安全集合:并发容器。
  • 任务排队:阻塞队列。
  • 线程复用:线程池。
  • 线程级上下文:ThreadLocal
  • 异步结果:Future

并发工具不是越多越好,关键是先说清楚协作模型,再选择最贴合的工具。