← 返回并发与锁
1. 为什么需要线程通信?
多线程不是各干各的,大部分场景下线程之间需要协同 — 传递数据、通知状态、等待结果。这就是"线程通信"。
| 场景 | 需求 | 典型例子 |
| 生产者-消费者 | 一个线程生产数据,另一个线程消费,中间用队列缓冲 | 日志采集:业务线程生产日志,异步线程落盘 |
| 任务分解合并 | 父线程把大任务拆成子任务,派给子线程,等所有子任务完成合并结果 | 批量查询 100 个用户,10 个线程并行查,合并返回 |
| 父等子结果 | 父线程派任务给子线程,阻塞等子线程结果 | 接口聚合:主线程调 3 个下游服务(并行),等结果合并返回 |
| 子通知父 | 子线程完成某事,通知父线程继续 | 启动初始化:子线程加载配置,完成后通知主线程开始接流量 |
| 流水线协同 | 多线程像流水线一样,前一步的输出是后一步的输入 | 数据处理:线程A读 → B解析 → C写库,各司其职 |
| 上下文传递 | 父线程的某些状态(用户ID/TraceId)要传给子线程 | 请求链路:主线程拿到 userId,子线程执行时也要知道是谁 |
核心难点:线程之间内存隔离 — 每个线程有自己的栈(局部变量私有),堆内存虽然共享但读写有可见性和竞态问题。线程通信本质就是解决两个问题:① 数据怎么传 ② 时机怎么同步。
2. 父子线程通信的特殊性 ⭐(重点)
"父子线程"是个常用说法,指一个线程(A)创建了另一个线程(B),A 是父,B 是子。但 Java 里父子线程有几个"反直觉"的坑,这部分讲透。
2.1 父子线程的内存关系
父线程 main
栈(局部变量私有)
→ 创建 →
子线程 Thread-1
栈(局部变量私有)
↑ 共享:堆内存(对象/数组) · static 变量 · ThreadLocal 默认 不共享
坑 1:局部变量不能直接传给子线程
父线程方法里的局部变量(比如 `int x = 10;`),子线程默认拿不到。因为局部变量在栈上,栈是线程私有的。子线程要用,只能通过构造参数 / Lambda 捕获(本质是复制)传进去。
坑 2:ThreadLocal 默认不继承
父线程 `threadLocal.set(userId)`,子线程 `threadLocal.get()` 得到的是 null!普通 ThreadLocal 是线程隔离的,父子也不通。这是面试高频坑。
2.2 线程池场景下的"父子"陷阱
实际工程里我们几乎不直接 `new Thread()`,而是用线程池。线程池下"父子"关系更复杂,因为线程会被复用。
主线程(请求线程)
ThreadLocal.set(userId)
→ submit →
线程池(worker-1)
线程复用,不是新建
⚠️ worker-1 上次执行过别人的任务,残留了别人的 userId
❌ new Thread() 场景
- 每次新建线程,InheritableThreadLocal 能继承父线程值
- 但创建线程开销大,实际不用
- 子线程只在创建那一刻继承,父线程后续修改同步不到
✅ 线程池场景(真实工程)
- 线程复用,不会重新触发继承
- InheritableThreadLocal 失效!worker 上的值是上次的残留
- 必须用 TransmittableThreadLocal(TTL) 解决
结论:父子线程通信的难点不在"传数据"(构造参数/Lambda 就行),而在上下文传递(userId/TraceId/链路信息)。这块在工程里是高频坑,后面第 9 节专门讲。
3. 方式 1:共享变量 + volatile
最基础的通信方式 — 多个线程共享一个变量,用 volatile 保证可见性。父线程改标志位,子线程看到后退出。
场景:优雅停机
主线程启动一个后台监控线程,主线程收到关闭信号时把标志位改为 false,监控线程看到后退出。
📋 volatile 共享标志位代码 点击展开
VolatileDemo.java
public class VolatileDemo {
// ⚠️ volatile:保证可见性
// 没有 volatile 的话,子线程可能永远看不到主线程把 running 改成 false
// (JIT 会把 running 缓存到寄存器/工作内存)
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
// 子线程:监控任务,running=true 时一直跑
new Thread(() -> {
while (running) {
// 监控逻辑
System.out.println("监控中...");
try { Thread.sleep(500); } catch (Exception e) {}
}
System.out.println("监控线程退出");
}).start();
// 主线程:等 2 秒后发停机信号
Thread.sleep(2000);
running = false; // ⚠️ 子线程立刻看到,退出循环
System.out.println("主线程已发停机信号");
}
}
优点
- 最简单,无锁,性能好
- 适合"单向通知"场景(标志位)
- volatile 保证可见性,不会死循环
缺点
- 只保证可见性,不保证原子性(`count++` 仍会丢更新)
- 不能传复杂数据,只能传基本类型/引用
- 没有"等待-通知"机制,子线程得轮询
- 轮询浪费 CPU,不及时
细节:volatile 只解决可见性。如果要做 `count++`,得用 `AtomicInteger` 或 `synchronized`。volatile 适合"一个线程写、多个线程读"的标志位场景。
4. 方式 2:synchronized + wait/notify
Java 最古老的线程通信机制 — Object 类的方法,配合 synchronized 使用。一个线程等待,另一个线程通知。
场景:生产者-消费者(经典)
生产者往 list 里放数据,满了就 wait;消费者从 list 取数据,空了就 wait;每次操作完 notify 对方。
📋 wait/notify 实现生产者消费者 点击展开
WaitNotifyDemo.java
public class WaitNotifyDemo {
private static final List<String> buffer = new ArrayList<>();
private static final int MAX = 5; // 缓冲区上限
public static void main(String[] args) {
// 生产者
new Thread(() -> {
while (true) {
synchronized (buffer) { // ⚠️ 必须 synchronized 同一把锁
while (buffer.size() == MAX) { // ⚠️ 用 while 不用 if(防虚假唤醒)
try { buffer.wait(); } // ⚠️ wait 会释放锁
catch (InterruptedException e) { return; }
}
buffer.add("data-" + System.currentTimeMillis());
System.out.println("生产,size=" + buffer.size());
buffer.notifyAll(); // ⚠️ 通知消费者(用 notifyAll 不用 notify)
}
}
}, "Producer").start();
// 消费者
new Thread(() -> {
while (true) {
synchronized (buffer) {
while (buffer.isEmpty()) {
try { buffer.wait(); }
catch (InterruptedException e) { return; }
}
System.out.println("消费: " + buffer.remove(0));
buffer.notifyAll(); // 通知生产者
}
}
}, "Consumer").start();
}
}
4 个关键细节(面试必问):
- 必须在 synchronized 块里调用 wait/notify,否则抛 IllegalMonitorStateException
- wait 会释放锁,sleep 不会 — 这是两者最大区别
- 用 while 不用 if 判断条件 — 防止"虚假唤醒"(线程可能没收到 notify 就醒)
- 用 notifyAll 不用 notify — notify 只叫醒一个,可能叫错(叫到同类线程导致死锁)
优点
- JDK 原生,无依赖
- 能实现等待-通知机制
- 适合简单的生产者-消费者
缺点
- 只能有一个等待队列(条件)
- notify 无法指定通知哪类线程
- 容易写错(忘 synchronized/用 if)
- 实际工程几乎不用,被 Condition/BlockingQueue 替代
5. 方式 3:Lock + Condition
wait/notify 的升级版 — JUC(Java.util.concurrent)提供,配合 ReentrantLock 使用。支持多个条件队列,能精确通知。
场景:阻塞队列(put/take 分开等待)
生产者等"非满",消费者等"非空" — 两个不同的条件,用 wait/notify 没法区分,但 Condition 可以。
📋 Lock + Condition 实现阻塞队列 点击展开
ConditionDemo.java
import java.util.concurrent.locks.*;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
// ⚠️ 两个条件:一个等"非满",一个等"非空"
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[5];
private int count, putIdx, takeIdx;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // ⚠️ 满了 → 等"非满"条件
}
items[putIdx] = item;
if (++putIdx == items.length) putIdx = 0;
count++;
notEmpty.signal(); // ⚠️ 精确通知"等非空"的线程
} finally {
lock.unlock(); // ⚠️ 必须在 finally 释放
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // ⚠️ 空了 → 等"非空"条件
}
Object x = items[takeIdx];
items[takeIdx] = null;
if (++takeIdx == items.length) takeIdx = 0;
--count;
notFull.signal(); // ⚠️ 精确通知"等非满"的线程
return x;
} finally {
lock.unlock();
}
}
}
vs wait/notify 的优势:① 多条件队列(notFull/notEmpty 分开等);② 精确通知(signal 唤醒指定条件的线程);③ 支持公平锁/可中断;④ ArrayBlockingQueue 底层就是这套实现。
优点
- 多条件队列,精确通知
- 支持中断/超时(await(timeout))
- 能实现公平锁
- JDK 阻塞队列底层就用它
缺点
- 必须 lock.lock() + finally unlock,容易忘
- 代码比 BlockingQueue 啰嗦
- 实际工程直接用 BlockingQueue,很少手写
6. 方式 4:BlockingQueue(生产者-消费者首选)
生产者-消费者场景的工程首选 — 把锁和等待封装好了,业务代码极简。
| 实现 | 特点 | 场景 |
| ArrayBlockingQueue | 数组实现,有界,单锁 | 生产者-消费者(队列长度固定) |
| LinkedBlockingQueue | 链表实现,无界(默认 Integer.MAX),双锁 | 线程池的默认队列 |
| SynchronousQueue | 不存元素,put 必须等 take | 直接传递(cachedThreadPool 用) |
| PriorityBlockingQueue | 优先级排序,无界 | 任务有优先级 |
| DelayQueue | 元素到期才能取 | 延迟任务/定时 |
📋 BlockingQueue 实现日志异步落盘 点击展开
AsyncLogger.java
import java.util.concurrent.*;
public class AsyncLogger {
// ⚠️ 有界队列:防止日志积压 OOM
private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(10000);
private volatile boolean running = true;
public AsyncLogger() {
// 消费线程:从队列取日志,写文件
new Thread(() -> {
while (running || !queue.isEmpty()) {
try {
// ⚠️ take 队列空时阻塞,put 队列满时阻塞
String log = queue.take(); // 阻塞获取
writeToFile(log);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "log-writer").start();
}
// 生产者:业务线程调用
public void log(String msg) {
// ⚠️ offer 不阻塞,队列满就丢弃(防业务线程被拖垮)
// 如果用 put,队列满了业务线程会阻塞 — 别这么干
boolean ok = queue.offer(msg);
if (!ok) {
System.err.println("日志队列满,丢弃: " + msg);
}
}
public void shutdown() {
running = false;
}
private void writeToFile(String log) { /* 写文件 */ }
}
关键细节:① 用 offer 不用 put(队列满别阻塞业务线程);② 队列要有界(无界队列 OOM);③ 消费线程要处理 InterruptedException(优雅停机)。
优点
- 开箱即用,代码极简
- 线程安全,锁封装好
- 5 种实现覆盖各种场景
- 线程池底层就用它
缺点
- 无界队列有 OOM 风险(LinkedBlockingQueue)
- 不能跨进程通信(那是 MQ 的事)
- 无回执(生产者不知道消费者处理结果)
7. 方式 5:CountDownLatch / CyclicBarrier / Semaphore
这三个是 JUC 的"同步辅助类",专门解决线程协同的特定场景。
| 工具 | 核心语义 | 能否复用 | 典型场景 |
| CountDownLatch | 等 N 个线程都完成(倒计数) | 不可复用 | 主线程等 N 个子任务完成 |
| CyclicBarrier | N 个线程互相等到齐,再一起走 | 可复用 | 多线程分阶段计算(类似开会) |
| Semaphore | 限制同时访问的线程数(信号量) | 可复用 | 限流:最多 5 个线程同时访问 |
📋 CountDownLatch:父线程等子线程完成 点击展开
CountDownLatchDemo.java
import java.util.concurrent.*;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 场景:接口聚合 — 并行查 3 个服务,等全部返回后合并
int n = 3;
// ⚠️ 初始计数 = 3,每完成一个 countDown 减 1
CountDownLatch latch = new CountDownLatch(n);
ExecutorService pool = Executors.newFixedThreadPool(n);
Map<String, Object> result = new ConcurrentHashMap<>();
for (int i = 0; i < n; i++) {
final int idx = i;
pool.submit(() -> {
try {
// 模拟调用下游服务
Thread.sleep(1000 + idx * 500);
result.put("svc" + idx, "data-" + idx);
System.out.println(Thread.currentThread().getName() + " 完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// ⚠️ 必须在 finally 里 countDown,防止异常导致主线程死等
latch.countDown();
}
});
}
// ⚠️ 主线程阻塞等待,最多等 5 秒(防止子任务卡死)
boolean done = latch.await(5, TimeUnit.SECONDS);
if (!done) {
System.err.println("部分子任务超时");
}
System.out.println("所有子任务完成,结果: " + result);
pool.shutdown();
}
}
📋 CyclicBarrier:多线程到齐后一起走 点击展开
CyclicBarrierDemo.java
import java.util.concurrent.*;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 场景:3 个线程分阶段处理数据,每阶段都等齐后再进下一阶段
// ⚠️ 第二个参数:所有线程到齐后执行的回调
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("=== 所有线程到齐,进入下一阶段 ===");
});
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
final int id = i;
pool.submit(() -> {
try {
System.out.println("线程" + id + " 阶段1开始");
Thread.sleep(1000 * (id + 1));
barrier.await(); // ⚠️ 等其他线程
System.out.println("线程" + id + " 阶段2开始");
Thread.sleep(500);
barrier.await(); // ⚠️ CyclicBarrier 可复用(区别于 CountDownLatch)
System.out.println("线程" + id + " 完成");
} catch (Exception e) {
Thread.currentThread().interrupt();
}
});
}
pool.shutdown();
}
}
CountDownLatch vs CyclicBarrier 区别:
- 语义不同:CountDownLatch 是"一个人等 N 个人";CyclicBarrier 是"N 个人互相等"
- 复用:CountDownLatch 用完即弃;CyclicBarrier 可 reset 重用
- 回调:CyclicBarrier 支持到齐后执行回调;CountDownLatch 不支持
8. 方式 6:Future / CompletableFuture(父子结果传递)
父线程派任务给子线程(线程池),等子线程返回结果 — 这是父子通信最高频场景。Future 是基础,CompletableFuture 是升级版(支持链式/组合)。
场景:接口聚合(并行调 3 个服务)
主线程要返回用户详情,需要查用户基本信息 + 订单 + 积分,3 个服务串行要 1.5s,并行只要 0.5s。
📋 CompletableFuture 并行调用 + 合并结果 点击展开
UserDetailService.java
import java.util.concurrent.*;
@Service
public class UserDetailService {
@Autowired private UserClient userClient;
@Autowired private OrderClient orderClient;
@Autowired private PointClient pointClient;
// ⚠️ 用独立的线程池,不要用 ForkJoinPool.commonPool(会被其他任务挤占)
private final ExecutorService pool = Executors.newFixedThreadPool(10);
public UserDetailVO getUserDetail(Long userId) throws Exception {
// ① 三个任务并行发起(CompletableFuture.supplyAsync 异步执行)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userClient.getUser(userId), pool);
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(
() -> orderClient.getOrders(userId), pool);
CompletableFuture<Integer> pointFuture = CompletableFuture.supplyAsync(
() -> pointClient.getPoints(userId), pool);
// ② 等三个任务都完成,合并结果
// ⚠️ allOf 返回 CompletableFuture,join 阻塞等待全部完成
CompletableFuture.allOf(userFuture, orderFuture, pointFuture).join();
// ③ 取结果(此时一定已完成,join 不会阻塞)
UserDetailVO vo = new UserDetailVO();
vo.setUser(userFuture.get()); // 已完成,立刻返回
vo.setOrders(orderFuture.get());
vo.setPoints(pointFuture.get());
return vo;
}
// 链式调用示例:查用户 → 查订单(依赖用户结果)
public UserDetailVO getUserThenOrders(Long userId) {
return CompletableFuture
.supplyAsync(() -> userClient.getUser(userId), pool)
.thenApplyAsync(user -> { // ⚠️ thenApply:用上一步结果继续算
List<Order> orders = orderClient.getOrders(user.getId());
UserDetailVO vo = new UserDetailVO();
vo.setUser(user);
vo.setOrders(orders);
return vo;
}, pool)
.exceptionally(ex -> { // ⚠️ 异常处理:任何一步失败都走这里
log.error("获取用户详情失败", ex);
return new UserDetailVO(); // 降级返回空对象
})
.join(); // 阻塞获取最终结果
}
}
CompletableFuture 关键方法:
supplyAsync — 异步执行有返回值的任务
thenApply / thenApplyAsync — 用上一步结果继续算(同步/异步)
thenCombine — 合并两个 future 的结果
allOf — 等所有 future 完成(用于并行聚合)
anyOf — 任一完成就返回(用于多源查询取最快)
exceptionally — 异常降级
join — 阻塞获取结果(get 抛检查异常,join 抛 RuntimeException)
优点
- 支持链式/组合,代码优雅
- 异常处理完善(exceptionally)
- 支持超时(orTimeout JDK9+)
- 父子结果传递首选
缺点
- 调试困难(调用链长)
- 默认用 ForkJoinPool.commonPool(共享,易被拖垮)
- join 阻塞要小心(可能死锁)
9. 方式 7:父子上下文传递(ThreadLocal / InheritableThreadLocal / TTL)⭐
这是父子线程通信里最隐蔽也最容易踩坑的部分 — 上下文(userId/TraceId/租户ID)怎么从父线程传到子线程。
| 方式 | 能否传子线程 | 能否传线程池 | 说明 |
| ThreadLocal | 不能 | 不能 | 线程隔离,父子也不通 |
| InheritableThreadLocal | 能(new Thread 时) | 不能 | 线程池下失效(线程复用) |
| TransmittableThreadLocal(TTL) | 能 | 能 | 阿里开源,工程首选 |
📋 ThreadLocal 默认不传子线程(踩坑演示) 点击展开
ThreadLocalTrap.java
public class ThreadLocalTrap {
private static final ThreadLocal<String> userId =
ThreadLocal.withInitial(() -> null);
public static void main(String[] args) throws Exception {
userId.set("user-001"); // 主线程 set
System.out.println("主线程: " + userId.get()); // user-001
new Thread(() -> {
// ⚠️ 子线程取到的是 null!ThreadLocal 线程隔离,父子也不通
System.out.println("子线程: " + userId.get()); // null
}).start();
Thread.sleep(100);
}
}
📋 InheritableThreadLocal:new Thread() 时能传,线程池下失效 点击展开
InheritableDemo.java
public class InheritableDemo {
// ⚠️ InheritableThreadLocal:子线程创建时会继承父线程的值
private static final InheritableThreadLocal<String> userId = new InheritableThreadLocal<>();
public static void main(String[] args) throws Exception {
userId.set("user-001");
// ✅ 场景 1:new Thread() — 子线程能拿到 user-001
new Thread(() -> {
System.out.println("new Thread 子线程: " + userId.get()); // user-001 ✅
}).start();
// ❌ 场景 2:线程池 — 子线程拿不到!
ExecutorService pool = Executors.newFixedThreadPool(2);
// ⚠️ 第一次提交:线程池新建 worker,此时继承父线程值 user-001
pool.submit(() -> System.out.println("pool 第1次: " + userId.get()));
Thread.sleep(100);
// ⚠️ 主线程改了值
userId.set("user-002");
// ⚠️ 第二次提交:worker 已存在(复用),不再触发继承
// 拿到的是 user-001(创建时的值),不是 user-002!❌
pool.submit(() -> System.out.println("pool 第2次: " + userId.get()));
pool.shutdown();
}
}
📋 TransmittableThreadLocal(TTL):线程池下也能传 ⭐ 点击展开
TtlDemo.java — 工程首选
// 依赖:com.alibaba:transmittable-thread-local
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import com.alibaba.ttl.threadpool.TtlExecutors;
public class TtlDemo {
// ⚠️ 1. 用 TransmittableThreadLocal 替换 ThreadLocal
private static final TransmittableThreadLocal<String> userId = new TransmittableThreadLocal<>();
public static void main(String[] args) throws Exception {
userId.set("user-001");
// ⚠️ 2. 线程池要用 TtlExecutors 包装
ExecutorService pool = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(2));
pool.submit(() -> {
System.out.println("第1次: " + userId.get()); // user-001 ✅
});
Thread.sleep(100);
userId.set("user-002"); // 主线程改值
pool.submit(() -> {
// ⚠️ TTL 在任务提交时"快照"父线程值,在任务执行时"回放"
System.out.println("第2次: " + userId.get()); // user-002 ✅
});
pool.shutdown();
}
}
/*
TTL 原理:
1. submit 时,TTL 把当前线程(提交者)的所有 TTL 变量快照一份
2. 任务执行前,TTL 把快照"回放"到执行线程
3. 任务执行后,TTL 把执行线程之前的值恢复(避免污染)
⚠️ 不用 TtlExecutors 包装的话,也可以用 TtlRunnable.get(runnable) 手动包装:
pool.submit(TtlRunnable.get(() -> { ... }));
*/
工程结论:需要把上下文(userId/TraceId/租户ID)传给子线程的,在工程上直接用 TTL + TtlExecutors 包装线程池。不要用 InheritableThreadLocal(线程池下失效)。Spring Cloud Sleuth / OpenTelemetry 的链路追踪就是基于类似机制。
TTL 优点
- 线程池下也能正确传递
- 每次提交都重新快照,父线程改值能同步
- 任务执行后自动恢复,不污染 worker
- 阿里开源,生产验证过
注意事项
- 线程池必须用 TtlExecutors 包装(否则失效)
- 用完后要 remove(防内存泄漏)
- 引入第三方依赖
- 异步编排(CompletableFuture)下要确认链路完整
10. 方式 8:Piped 管道流(了解即可)
Java 早期提供的线程间字节/字符传递方式 — PipedOutputStream + PipedInputStream。现代工程几乎不用,了解即可。
📋 PipedStream 示例(看一眼就行) 点击展开
PipedDemo.java
import java.io.*;
public class PipedDemo {
public static void main(String[] args) throws Exception {
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out); // 连接
// 线程 1:写管道
new Thread(() -> {
try {
out.write("hello from thread-1".getBytes());
out.close();
} catch (Exception e) {}
}).start();
// 线程 2:读管道
new Thread(() -> {
try {
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println("收到: " + new String(buf, 0, len));
} catch (Exception e) {}
}).start();
}
}
// ⚠️ 实际工程用 BlockingQueue 替代,API 更友好,功能更强
为什么不推荐:API 笨重(字节流),只能传字节/字符,不如 BlockingQueue 灵活。仅作为面试"知道有这东西"即可。
11. 8 种方式对比选型表
| 方式 |
适用场景 |
传数据 |
同步等待 |
推荐度 |
| volatile | 单向通知(停机标志) | 简单值 | 无(轮询) | ⭐⭐ 简单场景够用 |
| wait/notify | 生产者-消费者(经典) | 共享对象 | 有 | ⭐ 不推荐(用 Condition 替代) |
| Lock+Condition | 多条件队列 | 共享对象 | 有 | ⭐⭐⭐ 阻塞队列底层 |
| BlockingQueue | 生产者-消费者(工程) | 任意对象 | 有 | ⭐⭐⭐⭐⭐ 工程首选 |
| CountDownLatch | 等 N 个子任务完成 | 无 | 有 | ⭐⭐⭐⭐ 一次性等待 |
| CyclicBarrier | 多线程分阶段协同 | 无 | 有 | ⭐⭐⭐ 可复用 |
| CompletableFuture | 父子结果传递/异步编排 | 返回值 | 有(join) | ⭐⭐⭐⭐⭐ 父子结果首选 |
| TTL | 父子上下文传递 | 上下文 | 无 | ⭐⭐⭐⭐⭐ 线程池下必用 |
选型口诀
生产者-消费者 → BlockingQueue;父等子结果 → CompletableFuture;父子传上下文 → TTL;等 N 个完成 → CountDownLatch;通知标志位 → volatile。wait/notify 和管道流工程不用。
12. 使用陷阱 — 10 个高频坑
① wait/notify 不在 synchronized 块里
抛 IllegalMonitorStateException。wait/notify 必须持有对象锁才能调用。
② 用 if 不用 while 判断等待条件
虚假唤醒会导致条件不满足也继续执行。必须用 while 重新检查条件。
③ 用 notify 不用 notifyAll
notify 随机叫醒一个,可能叫到同类线程(生产者叫醒生产者),导致死锁。notifyAll 叫醒所有,虽然多点开销但安全。
④ BlockingQueue 用 put 不用 offer
put 队列满会阻塞生产者线程,业务线程被拖垮。用 offer(队列满返回 false,可丢弃或降级)。
⑤ LinkedBlockingQueue 无界导致 OOM
默认容量 Integer.MAX_VALUE,生产者快消费者慢时无限堆积,最终 OOM。必须 new LinkedBlockingQueue(1000) 显式设容量。
⑥ CountDownLatch 不在 finally countDown
子任务抛异常不 countDown,主线程 await 永远等不到。必须 finally countDown,或用带超时的 await。
⑦ CompletableFuture 用默认线程池
默认 ForkJoinPool.commonPool 全局共享,你的慢任务会拖垮其他人的并行流。必须传自定义线程池。
⑧ InheritableThreadLocal 在线程池下用
线程池线程复用,InheritableThreadLocal 只在线程创建时继承一次,后续主线程改值传不到。必须用 TTL。
⑨ ThreadLocal 不 remove 导致内存泄漏
线程池线程长期存活,ThreadLocal 引用的对象不释放。用完必须在 finally remove。Web 请求场景用拦截器统一清理。
⑩ CompletableFuture 链式调用里 join 嵌套
在 CompletableFuture 链里调另一个 future.join(),会占用 worker 线程阻塞等待,可能导致线程池耗尽死锁。用 thenCompose 替代嵌套 join。
13. 总结 — 场景速查 + 面试模板
速查表:场景 → 选型
| 我要做的事 | 用哪个 |
| 主线程等 N 个子任务并行完成,合并结果 | CompletableFuture.allOf + join |
| 主线程调子任务,等返回结果 | CompletableFuture.supplyAsync + join |
| 生产者-消费者(日志/事件) | BlockingQueue(ArrayBlockingQueue 有界) |
| 主线程等 N 个子任务完成(不需要结果) | CountDownLatch |
| 多线程分阶段协同 | CyclicBarrier |
| 限流(最多 N 个线程同时访问) | Semaphore |
| 父线程上下文(userId/TraceId)传子线程 | TTL + TtlExecutors |
| 停机标志位 | volatile |
| 原子计数 | AtomicInteger(不是 volatile) |
面试一句话模板
答题脚本
"Java 线程通信主要解决数据传递和时机同步两个问题。父子结果传递我用 CompletableFuture(并行聚合 + 链式调用);生产者-消费者用 BlockingQueue(注意有界 + offer);父子上下文传递用 TTL(线程池下 InheritableThreadLocal 会失效,这是个高频坑);等 N 个任务完成用 CountDownLatch(注意 finally countDown)。wait/notify 我不用,被 Condition/BlockingQueue 替代了。"
使用上要避坑:BlockingQueue 用 offer 不用 put(防业务线程阻塞)、CompletableFuture 用自定义线程池(防 commonPool 拖垮)、ThreadLocal 用完 remove(防内存泄漏)、线程池下用 TTL 替代 InheritableThreadLocal。
高频追问 FAQ
Q1:wait 和 sleep 的区别?
核心区别:wait 释放锁,sleep 不释放锁。① wait 是 Object 方法,必须在 synchronized 块里调用;sleep 是 Thread 静态方法,任何地方都能调。② wait 释放锁,sleep 不释放(抱着锁睡)。③ wait 唤醒靠 notify;sleep 靠超时或中断。④ wait 用于线程间通信;sleep 用于暂停当前线程。
Q2:为什么 wait 要用 while 不用 if?
防止虚假唤醒。线程可能在没有收到 notify 的情况下自己醒来(JVM 规范允许),如果用 if,醒来后不再检查条件直接执行,可能出错。用 while 会重新检查条件,虚假唤醒也能兜住。另外 notifyAll 会叫醒多个线程,只有一个能拿到锁,其他拿到锁时条件可能已不满足,必须 while 重新检查。
Q3:CompletableFuture 的 join 和 get 有什么区别?
① 异常类型:get 抛 InterruptedException + ExecutionException(检查异常,必须 catch);join 抛 CompletionException(RuntimeException,不强制 catch)。② 使用便利:Stream/lambda 里 join 更顺手(不用 try-catch)。③ 本质一样:都是阻塞等待结果。工程里推荐 join,代码更干净。
Q4:线程池下 InheritableThreadLocal 为什么失效?
线程池的 worker 线程是复用的,不是每次提交任务都新建。InheritableThreadLocal 只在线程创建时继承父线程的值,worker 第一次创建时继承,后续复用时不再继承 — 拿到的是上次任务的残留值。
解法:用 TransmittableThreadLocal(TTL) + TtlExecutors 包装线程池。TTL 在每次 submit 时快照父线程值,任务执行时回放,执行后恢复,保证每次任务都拿到正确的上下文。
Q5:CompletableFuture 怎么实现超时?
JDK 9+ 有 orTimeout(long, unit) — 超时自动以 TimeoutException 完成。completeOnTimeout(value, long, unit) — 超时返回默认值。
JDK 8 没有,要自己用 ScheduledExecutorService 配合 complete 实现:future.complete(...) 手动完成。
Q6:为什么 BlockingQueue 要用 offer 不用 put?
put 队列满会阻塞调用线程 — 如果是业务线程(比如 HTTP 请求线程),会被阻塞拖垮整个服务。offer 队列满立刻返回 false,业务线程可以决定丢弃/降级/记日志,不被阻塞。
例外:消费端用 take(队列空阻塞等)是 OK 的,因为消费线程就是专门干这个的。
最后一句话:线程通信的核心是"数据怎么传 + 时机怎么同步"。工程上记住五件套 — BlockingQueue(生产消费)/ CompletableFuture(父子结果)/ TTL(父子上下文)/ CountDownLatch(等 N 个)/ volatile(通知标志)。这五个能覆盖 95% 的场景,wait/notify 和管道流留给面试答理论就行。
线程通信实战 · 父子线程 + 8 种通信方式 · 并发与锁 / 07