并发工具类简介
概述
java.util.concurrent(常称 JUC)是 JDK 提供的并发工具包,在 synchronized 与 Lock 之外,提供了更丰富的线程协调与线程安全能力。本文介绍三类常用内容:同步器(CountDownLatch、CyclicBarrier、Semaphore)、原子类(AtomicInteger 等)以及并发集合的简要概念,帮助你在 线程池 等场景下正确选用工具,写出可靠的多线程代码。
JUC 包概览
| 类别 | 代表类 | 典型用途 |
|---|---|---|
| 同步器 | CountDownLatch、CyclicBarrier、Semaphore、Phaser | 多线程之间的「等待/放行」协调 |
| 原子类 | AtomicInteger、AtomicLong、AtomicReference | 无锁的线程安全计数、引用更新 |
| 锁与条件 | ReentrantLock、ReentrantReadWriteLock | 显式锁(详见 同步与锁) |
| 并发集合 | ConcurrentHashMap、BlockingQueue、CopyOnWriteArrayList | 线程安全的容器,替代同步包装 |
| 执行框架 | ExecutorService、ThreadPoolExecutor | 线程池(详见 线程池) |
下面重点介绍同步器和原子类的入门用法;并发集合仅做简要说明,便于你在实际项目中知道「该用哪一类」。
CountDownLatch:一次性倒计时
CountDownLatch 用于让一个或多个线程等待其它线程完成一定次数(N 次)的操作。内部维护一个计数器,构造时指定 N;其它线程每完成一次就调用 countDown() 将计数减 1;等待的线程通过 await() 阻塞,直到计数变为 0 后一起继续。
典型场景:主线程等待所有子任务完成后再做汇总、发令「所有运动员就位后同时开跑」等。
基本用法
// 构造:计数为 N
CountDownLatch latch = new CountDownLatch(N);
// 在「完成任务」的线程中:每完成一次就减 1
latch.countDown();
// 在「等待」的线程中:阻塞直到计数为 0
latch.await(); // 可被中断,抛出 InterruptedException
latch.await(2, TimeUnit.SECONDS); // 带超时,超时后不再等待示例:主线程等待多个子任务完成
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);
for (int i = 0; i < workerCount; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("Worker " + id + " 开始执行");
Thread.sleep(500 + id * 100L);
System.out.println("Worker " + id + " 完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 无论如何都减 1,避免主线程永远等不到
}
}).start();
}
System.out.println("主线程等待所有 Worker 完成...");
latch.await();
System.out.println("所有 Worker 已完成,主线程继续");
}
}提示
在 finally 中调用 countDown(),可以保证即使任务抛异常或提前 return,计数也会减 1,避免等待线程永远阻塞。
CyclicBarrier:可复用的栅栏
CyclicBarrier 让一组线程在某个「栅栏」处互相等待,直到所有线程都到达后,再一起继续执行。与 CountDownLatch 不同:CountDownLatch 是「一个减计数,其它人等」;CyclicBarrier 是「所有人到齐才一起走」,且可重复使用(下一轮再次 await 即可)。
典型场景:多轮迭代计算中,每轮都要等所有线程算完再进入下一轮;多人游戏中「准备就绪后同时开始」。
基本用法
// 构造:参与线程数 N,可选一个「到齐后」执行的动作
CyclicBarrier barrier = new CyclicBarrier(N);
CyclicBarrier barrierWithAction = new CyclicBarrier(N, () -> System.out.println("全部到齐,开始下一阶段"));
// 每个参与线程到达栅栏时调用
barrier.await(); // 阻塞直到 N 个线程都调用了 await
barrier.await(2, TimeUnit.SECONDS); // 带超时示例:多线程分阶段协作
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> System.out.println(">>> 本阶段所有人到齐,继续下一阶段"));
for (int i = 0; i < parties; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("线程 " + id + " 完成阶段一");
barrier.await();
System.out.println("线程 " + id + " 完成阶段二");
barrier.await();
System.out.println("线程 " + id + " 全部完成");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}注意
若有一个线程在 await() 时被中断或超时,或栅栏被 reset(),其它等待中的线程会收到 BrokenBarrierException,表示本轮栅栏已失效。使用带超时的 await 时要注意处理该异常并决定是否重试或退出。
Semaphore:信号量(限制并发数)
Semaphore 维护一组许可(permits)数量,线程在执行前先 acquire 获取许可(没有则阻塞),用完后 release 释放许可,从而限制同时执行某段代码的线程数。
典型场景:限制同时访问某资源的线程数(如数据库连接池、限流)、控制「最多 N 个线程进入临界区」。
基本用法
// 构造:许可数量
Semaphore sem = new Semaphore(2); // 最多 2 个线程同时持有许可
sem.acquire(); // 获取 1 个许可,没有则阻塞;可被中断
sem.release(); // 释放 1 个许可
sem.tryAcquire(); // 非阻塞,获取到返回 true
sem.tryAcquire(1, TimeUnit.SECONDS); // 等待最多 1 秒示例:限制同时执行的任务数
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 最多 2 个线程同时进入「使用资源」的逻辑
Semaphore sem = new Semaphore(2);
for (int i = 0; i < 5; i++) {
final int id = i;
new Thread(() -> {
try {
sem.acquire();
System.out.println("线程 " + id + " 获得许可,使用资源");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
sem.release();
System.out.println("线程 " + id + " 释放许可");
}
}).start();
}
}
}提示
release() 建议放在 finally 中,确保无论是否抛异常都会释放许可,避免许可泄漏导致后续线程永远拿不到许可。
原子类简介
当只需要对单个变量做线程安全的「读-改-写」(如自增、CAS 更新)时,不必加锁,可使用 java.util.concurrent.atomic 包下的原子类,底层基于 CAS(Compare-And-Swap) 实现无锁更新。
| 类 | 说明 |
|---|---|
| AtomicInteger、AtomicLong | 原子整型、长整型,提供 incrementAndGet()、addAndGet()、compareAndSet() 等 |
| AtomicReference<V> | 原子更新引用 |
| AtomicBoolean | 原子布尔 |
示例:用 AtomicInteger 替代同步计数
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter.incrementAndGet(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter.incrementAndGet(); });
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get()); // 稳定输出 20000
}
}在「高并发、简单计数或简单状态更新」场景下,原子类通常比 synchronized 或 Lock 更轻量,且能避免死锁;复杂复合操作仍需要锁或更高级的并发结构。
并发集合简要说明
JUC 还提供了一系列线程安全的集合,在多线程环境下可直接使用,而无需在外层再加 synchronized 或 Lock:
| 类型 | 代表类 | 简要说明 |
|---|---|---|
| Map | ConcurrentHashMap | 高并发下替代 Hashtable / 同步的 HashMap,分段或 CAS 优化 |
| Queue | BlockingQueue(如 LinkedBlockingQueue、ArrayBlockingQueue) | 阻塞队列,支持「队列空时取阻塞、满时放阻塞」,常与 线程池 和生产者-消费者配合 |
| List | CopyOnWriteArrayList | 写时复制,读多写少时线程安全且读无锁 |
具体 API 与选用场景可在学习完 集合 后,结合「多线程共享容器」的需求查阅官方文档或进阶资料。
注意事项
注意
- CountDownLatch 的计数只能减不能增,且不能重置,属于一次性使用;若需要多轮「等待 N 次完成」,应使用 CyclicBarrier 或新建多个 CountDownLatch。
- await() 会响应中断:当前线程在等待时若被中断,会抛出 InterruptedException。捕获后通常应恢复中断状态:
Thread.currentThread().interrupt(),由上层决定是否退出。
提示
- 同步器与 线程池 结合使用时,在任务中调用
latch.countDown()或barrier.await()即可,无需再「new Thread」;线程池负责线程复用与调度。 - 需要「多线程等一个结果」时用 CountDownLatch;需要「多线程互相等,到齐再走」且可能多轮时用 CyclicBarrier;需要「限制同时执行的线程数」时用 Semaphore。