← 返回 Java核心图解 目录

📡 Java线程间通信图解

面试核心三件套 · 协作工具 · 生产实战串讲 · 扩展知识 · 通信选型决策树

🗺️ 面试官问"线程通信",他到底想听什么?

面试官说"讲讲Java的线程间通信",你以为他在考你API?不,他想听的是你有没有真正在多线程环境里写过代码、踩过坑

🎯 面试官真正关心的三层
层次面试官想确认怎么回答加分
第一层:知道有哪些工具不是只会说synchronized"通信方式很多,按场景分:传数据用BlockingQueue、等完成用CompletableFuture、传上下文用ThreadLocal/TTL……"
第二层:知道生产中用哪个有实际项目经验,不是背书的"我们项目里最常用的是CompletableFuture做异步编排,BlockingQueue做异步任务队列"
第三层:知道坑在哪真的踩过坑,有深度"InheritableThreadLocal在线程池里会脏数据,我们改用了TTL";或者"吞InterruptedException导致优雅停机失效"
一句话回答框架

"线程通信本质是线程之间怎么协作。生产中按场景分4类:传数据用BlockingQueue,等完成/拿结果用CompletableFuture,传上下文用TransmittableThreadLocal,协调/叫停用CountDownLatch和interrupt。底层机制是wait/notify和Condition,但实际编码很少直接用。"

记住这句话,面试30秒就能让面试官知道你不是背书的。

线程间通信 怎么协作? 📦 传数据 生产者→消费者 BlockingQueue ⭐ 底层:wait/notify · Condition 极端:Exchanger(2人互换) 生产最高频 ⚡ 🔗 等结果 主线程等异步任务完成 CompletableFuture ⭐ 简单:Thread.join()(快淘汰了) N个任务:CF.allOf() 生产主力 ⚡ 👨‍👦 传上下文 父线程→子线程(userId/TraceId) TTL ⭐(线程池场景) new Thread:InheritableThreadLocal 不传给子线程:ThreadLocal 链路追踪必备 🔗 🤝 协调 / 叫停 CountDownLatch(1等N) CyclicBarrier(N互等) Semaphore(限并发) interrupt()(叫停) 面试常考 Latch/Barrier 区别 🔧 底层机制(面试常问原理) wait() / notify() / notifyAll() Condition.await() / signal() BlockingQueue 内部就是用 Condition 实现的 生产中一般不直接写,但面试爱问原理 📌 面试一句话总结 "传数据→BlockingQueue · 等结果→CompletableFuture · 传上下文→TTL · 协调→Latch · 叫停→interrupt" 5种场景覆盖面试+生产90%的需求
线程通信全景图:4大场景 + 底层机制,一图搞懂面试/生产怎么选
⚠️ 面试常见翻车点
翻车1:只会说synchronized — synchronized是同步(互斥),不是通信(协作)。面试官追问"还有呢?"就卡住了
翻车2:分不清同步和通信 — "同步"防止同时改(锁),"通信"协调执行顺序(通知/传数据)
翻车3:说不出生产用什么 — 面试官要的是"你们项目用了什么",不是API列表
翻车4:不知道线程池+ThreadLocal的坑 — InheritableThreadLocal在线程池会脏数据,这是高频追问点
PART 1
面试核心三件套
wait/notify(底层原理) → BlockingQueue(生产首选) → Condition(精准唤醒)

🔧 三件套:面试官必须听你讲完的三个东西

面试官问线程通信,你只要讲清楚这三个,就过线了。其他都是加分项。

🏥 1. wait/notify — 底层原理(面试必问)

面试官几乎一定会追问原理:"BlockingQueue底层怎么实现的?" 答案就是wait/notify。你不了解这个,面试就卡住了。

🏥 生活类比:医院叫号
Java医院说明
obj.wait()坐在候诊区等叫号释放锁,进入等待
obj.notify()护士叫一个号唤醒一个等待线程
obj.notifyAll()广播"XX科请进"唤醒所有等待线程
synchronized(obj)取号机(共享资源)必须在同步块内调用
线程A(等待方) ① 获取obj锁 ② 条件不满足 → wait() ③ 释放锁,进入wait set ④ 被notify唤醒,重新争锁 线程B(通知方) ① 获取obj锁 ② 修改条件 ③ notify() 通知 ④ 退出同步块,释放锁 Object Monitor 锁 + entrySet + waitSet wait() notify() 唤醒 ⚠️ wait()释放锁!notify()不释放锁!
wait/notify流程:等的人释放锁进等待区,通知的人改完条件叫号
BoundedBuffer.java — 手写wait/notify(面试经典题)
BoundedBuffer.java
public class BoundedBuffer<T> { private final Object lock = new Object(); private final List<T> list = new ArrayList<>(); private final int capacity; public BoundedBuffer(int capacity) { this.capacity = capacity; } public void put(T item) throws InterruptedException { synchronized (lock) { // ⚠️ 必须用while,不能用if!(虚假唤醒) while (list.size() == capacity) { lock.wait(); // 满了就等,释放锁 } list.add(item); lock.notifyAll(); // 通知消费者:有数据了 } } public T take() throws InterruptedException { synchronized (lock) { while (list.isEmpty()) { lock.wait(); // 空了就等 } T item = list.remove(0); lock.notifyAll(); // 通知生产者:有空位了 return item; } } } // 面试官追问"为什么用while不用if?" → 看FAQ
⚠️ 面试3连问,你必须答得出
Q1: 为什么wait()必须在synchronized内? — wait()要释放锁,你没拿到锁释放什么?
Q2: wait()用if还是while? — 必须while!防止虚假唤醒和被别人抢先
Q3: notify和notifyAll用哪个? — 生产用notifyAll(不会漏叫),精准场景用Condition.signal
👇 面试加分
"上面是底层原理。但生产中我不会手写wait/notify,因为有更好的封装——BlockingQueue。"

📬 2. BlockingQueue — 生产首选(用的最多)

面试讲完wait/notify,下一句应该是:"但实际开发我不手写,用BlockingQueue"。它把wait/notify封装好了,一行代码搞定生产者消费者。

📬 生活类比:快递柜
BlockingQueue快递柜说明
put(e)快递员往柜子里塞包裹柜子满了就等(阻塞)
take()你去柜子取包裹柜子空了就等(阻塞)
offer(e, 5, SECONDS)等5分钟塞不进去就走了超时返回false
poll(5, SECONDS)等5分钟没包裹就走了超时返回null
OrderProcessor.java — 生产环境真实写法(对比wait/notify)
OrderProcessor.java
// ✅ 生产首选:BlockingQueue,10行搞定生产者消费者 public class OrderProcessor { // 有界队列,防止OOM private final BlockingQueue<Order> queue = new ArrayBlockingQueue<>(1000); // 生产者:接单后丢进队列 public void submitOrder(Order order) { try { queue.put(order); // 满了自动阻塞,不用自己写wait/notify } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断 throw new RuntimeException("提交中断", e); } } // 消费者:后台线程不断取 public void startConsumer() { new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { try { Order order = queue.take(); // 空了自动阻塞 processOrder(order); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 优雅退出 } } }).start(); } } // 对比:手写wait/notify需要30行,BlockingQueue只要3行核心代码
6种BlockingQueue怎么选?记住2个就够
队列特点类比什么时候用
ArrayBlockingQueue有界,数组实现固定大小的快递柜生产者消费者(首选)
LinkedBlockingQueue可选有界,链表实现可扩展的快递柜线程池默认队列(Executors.newFixedThreadPool用的就是它)
SynchronousQueue无缓冲,手递手当面交接线程池CachedThreadPool用
PriorityBlockingQueue优先级排序VIP优先柜任务有优先级时
DelayQueue延迟出队定时快递定时任务、缓存过期
LinkedTransferQueue更高效的传递高性能场景(少用)

面试回答:"生产者消费者用ArrayBlockingQueue,线程池默认用LinkedBlockingQueue,其他看场景。"

👇 面试追问
"BlockingQueue内部怎么实现的?" → "用ReentrantLock + Condition"。一个notFull Condition,一个notEmpty Condition,这就是Condition存在的意义。

🎯 3. Condition — 精准唤醒(面试加分项)

Condition是wait/notify的升级版:能精准唤醒指定类型的线程。wait/notify只有一个候诊区(notifyAll叫醒所有人),Condition可以分科候诊区(signal只叫醒某一科的患者)。

🎯 生活类比:分科候诊区
维度wait/notify(1个候诊区)Condition(分科候诊区)
等待区数量1个(Object的waitSet)多个(每个Condition一个)
唤醒精度notifyAll叫醒所有人signal只叫醒某一科
依赖synchronizedReentrantLock
场景简单等待/通知多个条件独立等待
类比一个大候诊区广播叫号内科区叫内科,外科区叫外科
ConditionBuffer.java — 两个Condition精准唤醒(对比wait/notify)
ConditionBuffer.java
public class ConditionBuffer<T> { private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); // "不满"候诊区 private final Condition notEmpty = lock.newCondition(); // "不空"候诊区 private final Object[] items = new Object[100]; private int count = 0; public void put(T item) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 满了 → 在"不满"区等 items[count++] = item; notEmpty.signal(); // ✅ 精准叫醒"不空"区的消费者 } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); // 空了 → 在"不空"区等 T item = (T) items[--count]; notFull.signal(); // ✅ 精准叫醒"不满"区的生产者 return item; } finally { lock.unlock(); } } } // 对比:wait/notify用notifyAll叫醒所有人(含不需要的线程),Condition精准叫醒 // BlockingQueue内部就是这个实现,这就是为什么面试问"BlockingQueue底层"
💡 面试回答模板
面试官:"BlockingQueue底层怎么实现的?"
:"内部用ReentrantLock加两个Condition:notFull和notEmpty。生产者满了就在notFull上await,放完数据后signalnotEmpty叫醒消费者。比wait/notify的优势是精准唤醒——不会把不需要的线程也叫醒。"
PART 2
协作工具 + 异步编排
CountDownLatch/CyclicBarrier/Semaphore(协调) · CompletableFuture(异步主力)

🤝 协调工具 + 异步编排

🏁 1. 协调三兄弟:CountDownLatch / CyclicBarrier / Semaphore

这三个工具解决的是"多个线程怎么配合"的问题。面试最常考的是Latch和Barrier的区别

一表搞定三兄弟
维度CountDownLatchCyclicBarrierSemaphore
类比🚪 游乐园关门(等所有人出来)🥂 同学聚会(等人齐了开饭)🅿️ 停车场(限N个车位)
谁等谁1个线程等N个(主等子)N个互相等(到了一起走)不等,只是限流
核心方法countDown()减计数
await()等归零
await()报到+等acquire()获取许可
release()归还
能否复用❌ 一次性✅ 可循环✅ 可反复
生产场景主线程汇总子任务结果多线程分批处理限流、连接池
面试频率⭐⭐⭐⭐⭐⭐⭐
DashboardService.java — CountDownLatch:主线程等3个数据源全部查完
DashboardService.java
// 典型场景:大屏需要汇总3个数据源,全部查完才能展示 public Dashboard buildDashboard() throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); Map<String, Object> data = new ConcurrentHashMap<>(); // 3个线程并行查数据 executor.submit(() -> { try { data.put("sales", salesClient.query()); } finally { latch.countDown(); } // 查完减1 }); executor.submit(() -> { try { data.put("orders", orderClient.query()); } finally { latch.countDown(); } }); executor.submit(() -> { try { data.put("users", userClient.query()); } finally { latch.countDown(); } }); latch.await(5, TimeUnit.SECONDS); // 等全部完成(最多5秒) return new Dashboard(data); // 3个全查完了,组装大屏 } // 注意:如果用CompletableFuture.allOf()更好,见下面
👇 更好的方案
上面CountDownLatch的场景,CompletableFuture.allOf()也能做,而且能拿到每个任务的结果。这就是为什么说CF是"生产主力"。

🔗 2. CompletableFuture — 异步编排主力(生产最高频)

这是现代Java异步编程的核心。Spring Boot的@Async返回的就是CompletableFuture。面试问到"你怎么做异步编排",就是考这个。

🔗 生活类比:快递到了短信通知(不用在家傻等)
方法类比说明
supplyAsync()下单让快递送异步执行,有返回值
thenApply()快递到了→拆箱→验货上一步结果→下一步输入
thenCombine()2个快递都到了→一起组装合并两个异步结果
allOf()所有快递都到了→出门等N个全部完成
exceptionally()快递丢了→换一家买异常降级
join()在家等最后一刻同步等结果(阻塞)
OrderService.java — 并行查用户+查订单(生产最常用)
OrderService.java
// ❌ 串行:查用户1秒 + 查订单1秒 = 2秒 public OrderVO getOrderSerial(String orderId) { User user = userClient.getUser(orderId); // 等1秒 Order order = orderClient.getOrder(orderId); // 再等1秒 return new OrderVO(user, order); } // ✅ 并行:查用户 + 查订单同时进行,总共1秒 public CompletableFuture<OrderVO> getOrderParallel(String orderId) { CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userClient.getUser(orderId)); CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> orderClient.getOrder(orderId)); // 两个都完成后合并 return userFuture.thenCombine(orderFuture, (user, order) -> new OrderVO(user, order)) .exceptionally(ex -> { log.error("查询失败", ex); return OrderVO.empty(); // 降级 }); } // ✅ Controller里同步等结果(返回给前端) @GetMapping("/order/{id}") public OrderVO getOrder(@PathVariable String id) { return orderService.getOrderParallel(id).join(); // 异步执行+同步等 }
💡 生产场景:Spring Boot @Async + CompletableFuture
Spring Boot异步标准写法:① 方法加@Async返回CompletableFuture<T> ② 调用方用thenApply/thenAccept处理结果 ③ Controller里.join()等结果返回前端

注意:@Async方法必须在不同的类调用才生效(Spring AOP代理限制),跟@Transactional同一个坑。
PART 3
生产实战串讲
4个真实场景,把前面所有工具串起来用

🏭 4个生产场景:把工具串起来用

面试官最爱问"你项目里怎么用的"。下面4个场景覆盖了90%的生产多线程通信需求

场景1:异步查询接口(CompletableFuture + thenCombine)

业务需求:订单详情页需要同时查用户信息、订单信息、物流信息,三个接口各耗时500ms。

1
❌ 串行:3个接口排队调用
总耗时 = 500ms × 3 = 1.5秒。用户等半天。
2
✅ 并行:3个CompletableFuture同时跑
supplyAsync()启动3个异步查询 → allOf()等全部完成 → join()拿结果。总耗时 = 500ms(最慢的那个)。
3
🛡️ 降级:exceptionally处理异常
如果物流接口挂了,exceptionally()返回空物流信息,不影响主流程。用户看到"物流信息暂不可查"而不是白屏。
场景2:异步任务队列(BlockingQueue + interrupt)

业务需求:用户上传Excel(10万行),后台异步处理,不能阻塞HTTP请求。

1
📥 接单:Controller收到请求
不直接处理Excel,而是把任务对象put()到ArrayBlockingQueue(容量1000),立即返回"处理中"。
2
⚙️ 消费:后台线程不断take()
消费者线程while(!isInterrupted()) { queue.take(); process(); },队空了自动阻塞等待。
3
🛑 停机:Spring Boot优雅停机
kill进程时Spring调thread.interrupt(),消费者在take()阻塞时抛InterruptedException → catch里必须重新interrupt() → while检查到中断退出 → 清理资源。如果吞了异常,优雅停机就失效了。
场景3:链路追踪传TraceId(TransmittableThreadLocal)

业务需求:全链路日志要有TraceId,但异步线程/线程池里TraceId丢了,日志断了。

1
🔍 问题:ThreadLocal不传给子线程
请求进来在Filter里ThreadLocal.set(traceId),但Controller调@Async方法进了线程池,新线程ThreadLocal.get()是null。日志没TraceId。
2
❌ 错误方案:InheritableThreadLocal
改用ITL,new Thread()场景能传。但线程池复用线程时不触发复制!请求A的TraceId可能串到请求B,日志全乱了。
3
✅ 正确方案:TransmittableThreadLocal + TtlExecutors
① Filter中TTL.set(traceId) ② 线程池用TtlExecutors.getTtlExecutorService()包装 ③ 每次submit自动捕获+还原上下文 ④ 请求结束TTL.remove()。TraceId在异步线程里也正确。
场景4:大屏数据汇总(CountDownLatch → 或CompletableFuture.allOf)

业务需求:运营大屏需要汇总销售、库存、用户3个数据源,全部查完才能渲染。

1
方案A:CountDownLatch
3个线程并行查,各自查完countDown(),主线程await(5, SECONDS)等全部完成。缺点:拿不到每个任务的返回值,需要自己用Map存。
2
方案B:CompletableFuture.allOf(推荐)
CF.supplyAsync(查销售) + CF.supplyAsync(查库存) + CF.supplyAsync(查用户)CF.allOf(cf1,cf2,cf3).join()。每个CF都能.join()拿返回值,还有exceptionally()降级。更优雅
3
面试怎么说
"这种场景我会优先用CompletableFuture.allOf,因为能直接拿到每个任务的结果。CountDownLatch也行,但需要自己维护结果Map,代码更臃肿。"
🎯 面试回答的黄金模板

面试官问"你们项目怎么用多线程",不要背API,用场景回答:

"我们项目有几个典型场景:① 接口异步编排用CompletableFuture,比如查订单详情并行查用户+订单+物流;② 异步任务队列用BlockingQueue,比如Excel批量导入丢队列后台处理;③ 链路追踪用TTL在线程池间传TraceId。"

这样回答面试官立刻知道你有实战经验。

PART 4
扩展知识
interrupt / ThreadLocal父子传递 / Exchanger / join — 面试追问备用

📚 扩展知识:面试追问时拿出来加分

这些不是核心,但面试官追深的时会问。了解了能加分。

🛑 interrupt() — 告诉线程"停一下"(优雅停机核心)

🛑 核心认知

interrupt()不是"杀线程",是礼貌地请求线程停止。线程在运行 → 设flag=true,需自己检查;线程在wait/sleep → 抛InterruptedException。

Java现实说明
thread.interrupt()拍同事肩膀说"别做了"只是通知,不是强停
Thread.interrupted()同事回头看一眼检查并清除中断标志
InterruptedException同事正在打电话,被你一拍挂了wait/sleep/join时被interrupt会抛异常
Thread.stop()💥 把同事拽出工位已废弃!数据不一致、锁不释放
⚠️ 最大坑:吞InterruptedException
InterruptedException抛出时JVM会清除中断标志。如果你catch后不重新interrupt(),上层代码检查isInterrupted()会得到false——Spring Boot优雅停机就这样失效了

catch (InterruptedException e) { log.error(e); } → flag丢了
catch (InterruptedException e) { Thread.currentThread().interrupt(); } → 恢复flag

👨‍👦 ThreadLocal父子传递 — 链路追踪核心(高频追问)

3种ThreadLocal对比
维度ThreadLocalInheritableThreadLocalTransmittableThreadLocal
类比私房钱(不给任何人)传家宝(只给亲生儿子)快递员代取(线程池也传)
父子传递❌ 不传✅ new Thread时复制✅ submit时捕获+还原
线程池❌ 复用线程不触发复制✅ 包装后自动处理
生产推荐不需要传时用❌ 容易踩坑✅ 线程池首选
⚠️ InheritableThreadLocal + 线程池 = 脏数据定时炸弹
ITL只在新建线程时复制。线程池复用线程——第一次submit可能碰巧复制了,第二次submit用同一个线程不会再复制!请求B可能读到请求A的userId。
铁律:有线程池就用TTL,别用ITL。

🔄 Exchanger — 两个线程互换数据(了解即可)

🔄 生活类比:互换礼物

Exchanger是最简单的通信——两个线程在汇合点交换数据。先到的线程阻塞,等另一个到了,互换。

典型场景:遗传算法(交换染色体)、数据校验(生成+校验互换数据)、游戏(交换牌)。

注意:只有2个线程时用,多个线程配对不确定。面试很少考,了解即可。

🔗 Thread.join() — 最原始的"等线程结束"(快被淘汰了)

join() vs CompletableFuture
维度Thread.join()CompletableFuture
等待时❌ 阻塞傻等✅ 异步回调
拿返回值❌ 拿不到✅ 有返回值
组合多个❌ 只能一个个join✅ allOf/anyOf/thenCombine
异常处理❌ 没有机制✅ exceptionally/handle
类比在家傻等快递快递到了短信通知→拆箱
生产推荐❌ 基本不用了✅ 生产首选

面试回答:"join()是最原始的等法,现在都用CompletableFuture了。"

🗺️ 线程通信选型决策树

你的线程通信需求是什么? 传数据 等结果 协调/叫停 传上下文 📦 传数据 BlockingQueue ⭐ 生产者→消费者 异步任务队列 极端:Exchanger(2人互换) ⚡生产最高频 🔗 等结果 CompletableFuture ⭐ 异步查数据+合并 @Async异步方法 旧:Thread.join()(快淘汰) ⚡生产主力 🤝 协调/叫停 CountDownLatch(1等N) CyclicBarrier(N互等) Semaphore(限并发) interrupt()(优雅停机) 👨‍👦 传上下文 TTL ⭐(线程池场景) TraceId、UserId、租户ID new Thread: InheritableTL 不传: ThreadLocal 🔗链路追踪必备 🔧 底层机制(面试问原理时答) wait/notify(Object级别) | Condition(Lock级别,BlockingQueue内部用) 🧭 选型口诀 传数据→BlockingQueue | 等结果→CF | 协调→Latch/Barrier | 叫停→interrupt | 传上下文→TTL 🏆 面试/生产最高频:BlockingQueue > CompletableFuture > TTL > CountDownLatch 📋 生产组合:CF异步查数据 | BlockingQueue+interrupt任务队列 | TTL传TraceId | @Async异步方法
4大场景选型决策树:一看就知道该用什么
🎯 通信选型五原则 1. 能用BlockingQueue就别手写wait/notify — 封装好了,不容易出错
2. 异步等结果用CompletableFuture,别join()傻等 — CF链式组合+异常处理才是生产标准
3. 精准唤醒用Condition,全场广播用notifyAll — 别用notify(可能叫错人)
4. 线程池传上下文必须用TTL — InheritableThreadLocal会脏数据
5. catch InterruptedException必须恢复中断 — 否则优雅停机失效

⚡ 速查表:一张表覆盖所有场景

场景首选工具类比一句话
生产者→消费者BlockingQueue📦 快递柜put() / take(),生产最高频
异步查数据+合并CompletableFuture🔗 快递到了通知supplyAsync → thenCombine
主线程等N个任务CountDownLatch (或CF.allOf)🏁 关门等所有人countDown() / await()
N线程互等到齐CyclicBarrier🥂 聚会等人齐await(),可循环复用
限并发数Semaphore🅿️ 车位牌acquire() / release()
精准唤醒指定线程Condition🎯 分科候诊区await() / signal()
父子线程传上下文(线程池)TransmittableThreadLocal👨‍👦 快递员代取set/get/remove,TtlExecutors包装
父子线程传上下文(new Thread)InheritableThreadLocal👨‍👦 传家宝给儿子⚠️线程池场景会脏数据
告诉线程停下来Thread.interrupt()🛑 拍肩膀叫停优雅停机核心
简单等待/通知(了解原理)wait/notify🏥 候诊区广播面试问原理,生产不用
等线程结束(快淘汰)Thread.join()🏠 傻等快递用CF.join()替代
两人互换数据(冷门)Exchanger🔄 互换礼物只有2个线程,少用

❓ FAQ

为什么wait()必须在synchronized块内?
因为wait()的本质是"释放当前对象的锁并等待"。你先得拥有锁,才能释放锁。就像你得先取了号,护士才能叫你的号。如果在synchronized外面调wait(),你根本没持有锁,释放什么?→ IllegalMonitorStateException。
wait()用if还是while?为什么?
必须用while,不能用if。三个原因:
虚假唤醒:即使没有notify,线程也可能被操作系统唤醒
被别人抢先:多个消费者被notifyAll唤醒,第一个拿到数据,第二个条件已不满足
条件已变:从wait()返回到重新获取锁之间,条件可能已被改变

if (count == 0) wait(); → 醒来不检查 → 可能出错
while (count == 0) wait(); → 醒来重新检查 → 安全
CountDownLatch和CyclicBarrier都能等,有什么区别?
核心区别:
- CountDownLatch:1个线程等N个。countDown()减计数,await()等归零。一次性的,用完不能重置。
- CyclicBarrier:N个线程互相等。各自await(),人到齐了触发barrierAction。可循环,用完自动重置。

选型:"主线程汇总子任务" → CountDownLatch;"所有线程到齐了一起出发" → CyclicBarrier。
但现在更推荐用CompletableFuture.allOf()替代CountDownLatch——能直接拿返回值。
catch InterruptedException后为什么要重新interrupt()?
因为InterruptedException抛出时,JVM会清除中断标志。
线程在wait/sleep/join时被interrupt → 抛异常 → flag被自动清除为false。上层代码检查isInterrupted()会得到false,以为没被中断。

catch (InterruptedException e) { log.error(e); } → flag丢了,Spring Boot优雅停机失效
catch (InterruptedException e) { Thread.currentThread().interrupt(); } → 恢复flag

铁律:要么catch后重新interrupt(),要么不catch直接throws。千万别吞了!
InheritableThreadLocal + 线程池为什么脏数据?
ITL只在新建线程时复制父线程的值。线程池的线程是复用的
① 第一次submit,碰巧线程池新建线程 → 复制了父线程值 ✅
② 第二次submit,线程池复用了同一个线程 → 不触发复制 → 子线程还是第一次的旧值 ❌

结果:请求B可能读到请求A的userId/TraceId,数据串了。
解决方案:用TransmittableThreadLocal + TtlExecutors包装线程池,每次submit自动捕获+还原上下文。
线程通信和线程同步有什么区别?
同步=互斥(防止同时改),通信=协作(协调执行顺序)。
- synchronized / Lock = 同步(你用完我再用,防止数据竞争)
- wait/notify / BlockingQueue / CF = 通信(你做完了告诉我,我再做下一步)

关系:通信通常依赖同步。wait()必须在synchronized内,BlockingQueue内部有ReentrantLock。先保证安全(同步),再协调顺序(通信)。
CompletableFuture的join()和Thread.join()有什么区别?
虽然名字一样,但完全不同的东西:
- Thread.join():等一个线程结束。没有返回值,只是"等它死"。
- CF.join():等一个异步计算完成,返回计算结果。异常时抛CompletionException。

生产中:Thread.join()几乎不用了,CF.join()很常见——在Controller里等异步结果返回给前端。
BlockingQueue底层怎么实现的?(面试高频)
ReentrantLock + 两个Condition:notFull 和 notEmpty。

以ArrayBlockingQueue为例:
put()时如果数组满了 → 在notFull上await(释放锁,等待)
take()时取走一个元素 → signal notFull(叫醒等待put的生产者)
③ 同理,take时如果空了 → 在notEmpty上await;put后signal notEmpty

比wait/notify优势:两个Condition精准唤醒(put完只叫醒消费者,不叫醒其他生产者),wait/notify用notifyAll会叫醒所有人。
什么场景下必须用Condition而不是BlockingQueue?
实际开发中,99%的情况BlockingQueue就够用了。
只有在你需要自定义同步器(类似AQS的实现),或者需要多个独立等待条件时,才直接用Condition。

面试回答:"BlockingQueue内部就是用Condition实现的,日常用BlockingQueue就行。需要自己写同步器时才直接用Condition,但这种情况很少。"
面试被问"线程通信"怎么30秒拿高分?
三步走回答:

第一步(10秒):"线程通信本质是线程之间的协作,分4类场景——"
传数据用BlockingQueue,等结果用CompletableFuture,传上下文用TTL,协调用CountDownLatch

第二步(10秒):"底层机制是wait/notify和Condition,BlockingQueue内部就是用Condition实现的。"

第三步(10秒):"我们项目里最常用的是CompletableFuture做异步编排,比如查订单详情并行查用户+订单+物流,从串行1.5秒优化到并行500ms。"

这样回答面试官立刻知道:①你懂分类 ②你懂底层原理 ③你有实战经验。不是背API的。