← 返回并发与锁

🔗 线程通信实战

父子线程通信 + 8 种通信方式 · 场景需求 · 代码注释 · 注意细节 · 优缺点陷阱

父子线程 wait/notify BlockingQueue CompletableFuture ThreadLocal 线程池坑 TTL

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 共享标志位代码 点击展开
结构
📦 demo
VolatileDemo.java
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 实现生产者消费者 点击展开
结构
📦 demo
WaitNotifyDemo.java
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 个关键细节(面试必问):
  1. 必须在 synchronized 块里调用 wait/notify,否则抛 IllegalMonitorStateException
  2. wait 会释放锁,sleep 不会 — 这是两者最大区别
  3. 用 while 不用 if 判断条件 — 防止"虚假唤醒"(线程可能没收到 notify 就醒)
  4. 用 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 实现阻塞队列 点击展开
结构
📦 demo
ConditionDemo.java
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 实现日志异步落盘 点击展开
结构
📦 log
AsyncLogger.java
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 个子任务完成
CyclicBarrierN 个线程互相等到齐,再一起走可复用多线程分阶段计算(类似开会)
Semaphore限制同时访问的线程数(信号量)可复用限流:最多 5 个线程同时访问
📋 CountDownLatch:父线程等子线程完成 点击展开
场景
主线程
↦ 等 N 个子任务
📁 子任务
查用户
查订单
查积分
↦ 全部完成后合并
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:多线程到齐后一起走 点击展开
场景
阶段 1: 各自查数据
↧ 到齐 ↧
阶段 2: 各自计算
↧ 到齐 ↧
阶段 3: 合并结果
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 区别:
  1. 语义不同:CountDownLatch 是"一个人等 N 个人";CyclicBarrier 是"N 个人互相等"
  2. 复用:CountDownLatch 用完即弃;CyclicBarrier 可 reset 重用
  3. 回调:CyclicBarrier 支持到齐后执行回调;CountDownLatch 不支持

8. 方式 6:Future / CompletableFuture(父子结果传递)

父线程派任务给子线程(线程池),等子线程返回结果 — 这是父子通信最高频场景。Future 是基础,CompletableFuture 是升级版(支持链式/组合)。

场景:接口聚合(并行调 3 个服务)

主线程要返回用户详情,需要查用户基本信息 + 订单 + 积分,3 个服务串行要 1.5s,并行只要 0.5s。

📋 CompletableFuture 并行调用 + 合并结果 点击展开
场景
📦 api
UserDetailService.java
主线程: 发起
子线程1: 查用户
子线程2: 查订单
子线程3: 查积分
主线程: 合并返回
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 默认不传子线程(踩坑演示) 点击展开
踩坑
📦 demo
ThreadLocalTrap.java
⚠️ 子线程取到 null
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() 时能传,线程池下失效 点击展开
场景
✅ new Thread 有效
❌ 线程池失效
📦 demo
InheritableDemo.java
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):线程池下也能传 ⭐ 点击展开
TTL
阿里开源
📦 demo
TtlDemo.java
✅ 线程池下正确
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 示例(看一眼就行) 点击展开
管道流
📦 demo
PipedDemo.java
现代工程不用
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;通知标志位 → volatilewait/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