面试核心三件套 · 协作工具 · 生产实战串讲 · 扩展知识 · 通信选型决策树
面试官说"讲讲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。你不了解这个,面试就卡住了。
| Java | 医院 | 说明 |
|---|---|---|
obj.wait() | 坐在候诊区等叫号 | 释放锁,进入等待 |
obj.notify() | 护士叫一个号 | 唤醒一个等待线程 |
obj.notifyAll() | 广播"XX科请进" | 唤醒所有等待线程 |
synchronized(obj) | 取号机(共享资源) | 必须在同步块内调用 |
面试讲完wait/notify,下一句应该是:"但实际开发我不手写,用BlockingQueue"。它把wait/notify封装好了,一行代码搞定生产者消费者。
| BlockingQueue | 快递柜 | 说明 |
|---|---|---|
put(e) | 快递员往柜子里塞包裹 | 柜子满了就等(阻塞) |
take() | 你去柜子取包裹 | 柜子空了就等(阻塞) |
offer(e, 5, SECONDS) | 等5分钟塞不进去就走了 | 超时返回false |
poll(5, SECONDS) | 等5分钟没包裹就走了 | 超时返回null |
| 队列 | 特点 | 类比 | 什么时候用 |
|---|---|---|---|
| ArrayBlockingQueue ⭐ | 有界,数组实现 | 固定大小的快递柜 | 生产者消费者(首选) |
| LinkedBlockingQueue ⭐ | 可选有界,链表实现 | 可扩展的快递柜 | 线程池默认队列(Executors.newFixedThreadPool用的就是它) |
| SynchronousQueue | 无缓冲,手递手 | 当面交接 | 线程池CachedThreadPool用 |
| PriorityBlockingQueue | 优先级排序 | VIP优先柜 | 任务有优先级时 |
| DelayQueue | 延迟出队 | 定时快递 | 定时任务、缓存过期 |
| LinkedTransferQueue | 更高效的传递 | — | 高性能场景(少用) |
面试回答:"生产者消费者用ArrayBlockingQueue,线程池默认用LinkedBlockingQueue,其他看场景。"
Condition是wait/notify的升级版:能精准唤醒指定类型的线程。wait/notify只有一个候诊区(notifyAll叫醒所有人),Condition可以分科候诊区(signal只叫醒某一科的患者)。
| 维度 | wait/notify(1个候诊区) | Condition(分科候诊区) |
|---|---|---|
| 等待区数量 | 1个(Object的waitSet) | 多个(每个Condition一个) |
| 唤醒精度 | notifyAll叫醒所有人 | signal只叫醒某一科 |
| 依赖 | synchronized | ReentrantLock |
| 场景 | 简单等待/通知 | 多个条件独立等待 |
| 类比 | 一个大候诊区广播叫号 | 内科区叫内科,外科区叫外科 |
这三个工具解决的是"多个线程怎么配合"的问题。面试最常考的是Latch和Barrier的区别。
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 类比 | 🚪 游乐园关门(等所有人出来) | 🥂 同学聚会(等人齐了开饭) | 🅿️ 停车场(限N个车位) |
| 谁等谁 | 1个线程等N个(主等子) | N个互相等(到了一起走) | 不等,只是限流 |
| 核心方法 | countDown()减计数 await()等归零 | await()报到+等 | acquire()获取许可 release()归还 |
| 能否复用 | ❌ 一次性 | ✅ 可循环 | ✅ 可反复 |
| 生产场景 | 主线程汇总子任务结果 | 多线程分批处理 | 限流、连接池 |
| 面试频率 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
这是现代Java异步编程的核心。Spring Boot的@Async返回的就是CompletableFuture。面试问到"你怎么做异步编排",就是考这个。
| 方法 | 类比 | 说明 |
|---|---|---|
supplyAsync() | 下单让快递送 | 异步执行,有返回值 |
thenApply() | 快递到了→拆箱→验货 | 上一步结果→下一步输入 |
thenCombine() | 2个快递都到了→一起组装 | 合并两个异步结果 |
allOf() | 所有快递都到了→出门 | 等N个全部完成 |
exceptionally() | 快递丢了→换一家买 | 异常降级 |
join() | 在家等最后一刻 | 同步等结果(阻塞) |
@Async返回CompletableFuture<T> ② 调用方用thenApply/thenAccept处理结果 ③ Controller里.join()等结果返回前端面试官最爱问"你项目里怎么用的"。下面4个场景覆盖了90%的生产多线程通信需求。
业务需求:订单详情页需要同时查用户信息、订单信息、物流信息,三个接口各耗时500ms。
supplyAsync()启动3个异步查询 → allOf()等全部完成 → join()拿结果。总耗时 = 500ms(最慢的那个)。exceptionally()返回空物流信息,不影响主流程。用户看到"物流信息暂不可查"而不是白屏。业务需求:用户上传Excel(10万行),后台异步处理,不能阻塞HTTP请求。
put()到ArrayBlockingQueue(容量1000),立即返回"处理中"。while(!isInterrupted()) { queue.take(); process(); },队空了自动阻塞等待。thread.interrupt(),消费者在take()阻塞时抛InterruptedException → catch里必须重新interrupt() → while检查到中断退出 → 清理资源。如果吞了异常,优雅停机就失效了。业务需求:全链路日志要有TraceId,但异步线程/线程池里TraceId丢了,日志断了。
ThreadLocal.set(traceId),但Controller调@Async方法进了线程池,新线程ThreadLocal.get()是null。日志没TraceId。new Thread()场景能传。但线程池复用线程时不触发复制!请求A的TraceId可能串到请求B,日志全乱了。TTL.set(traceId) ② 线程池用TtlExecutors.getTtlExecutorService()包装 ③ 每次submit自动捕获+还原上下文 ④ 请求结束TTL.remove()。TraceId在异步线程里也正确。业务需求:运营大屏需要汇总销售、库存、用户3个数据源,全部查完才能渲染。
countDown(),主线程await(5, SECONDS)等全部完成。缺点:拿不到每个任务的返回值,需要自己用Map存。CF.supplyAsync(查销售) + CF.supplyAsync(查库存) + CF.supplyAsync(查用户) → CF.allOf(cf1,cf2,cf3).join()。每个CF都能.join()拿返回值,还有exceptionally()降级。更优雅。面试官问"你们项目怎么用多线程",不要背API,用场景回答:
"我们项目有几个典型场景:① 接口异步编排用CompletableFuture,比如查订单详情并行查用户+订单+物流;② 异步任务队列用BlockingQueue,比如Excel批量导入丢队列后台处理;③ 链路追踪用TTL在线程池间传TraceId。"
这样回答面试官立刻知道你有实战经验。
这些不是核心,但面试官追深的时会问。了解了能加分。
interrupt()不是"杀线程",是礼貌地请求线程停止。线程在运行 → 设flag=true,需自己检查;线程在wait/sleep → 抛InterruptedException。
| Java | 现实 | 说明 |
|---|---|---|
thread.interrupt() | 拍同事肩膀说"别做了" | 只是通知,不是强停 |
Thread.interrupted() | 同事回头看一眼 | 检查并清除中断标志 |
InterruptedException | 同事正在打电话,被你一拍挂了 | wait/sleep/join时被interrupt会抛异常 |
Thread.stop() | 💥 把同事拽出工位 | 已废弃!数据不一致、锁不释放 |
| 维度 | ThreadLocal | InheritableThreadLocal | TransmittableThreadLocal |
|---|---|---|---|
| 类比 | 私房钱(不给任何人) | 传家宝(只给亲生儿子) | 快递员代取(线程池也传) |
| 父子传递 | ❌ 不传 | ✅ new Thread时复制 | ✅ submit时捕获+还原 |
| 线程池 | ❌ | ❌ 复用线程不触发复制 | ✅ 包装后自动处理 |
| 生产推荐 | 不需要传时用 | ❌ 容易踩坑 | ✅ 线程池首选 |
Exchanger是最简单的通信——两个线程在汇合点交换数据。先到的线程阻塞,等另一个到了,互换。
典型场景:遗传算法(交换染色体)、数据校验(生成+校验互换数据)、游戏(交换牌)。
注意:只有2个线程时用,多个线程配对不确定。面试很少考,了解即可。
| 维度 | Thread.join() | CompletableFuture |
|---|---|---|
| 等待时 | ❌ 阻塞傻等 | ✅ 异步回调 |
| 拿返回值 | ❌ 拿不到 | ✅ 有返回值 |
| 组合多个 | ❌ 只能一个个join | ✅ allOf/anyOf/thenCombine |
| 异常处理 | ❌ 没有机制 | ✅ exceptionally/handle |
| 类比 | 在家傻等快递 | 快递到了短信通知→拆箱 |
| 生产推荐 | ❌ 基本不用了 | ✅ 生产首选 |
面试回答:"join()是最原始的等法,现在都用CompletableFuture了。"
| 场景 | 首选工具 | 类比 | 一句话 |
|---|---|---|---|
| 生产者→消费者 | 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个线程,少用 |
countDown()减计数,await()等归零。一次性的,用完不能重置。await(),人到齐了触发barrierAction。可循环,用完自动重置。put()时如果数组满了 → 在notFull上await(释放锁,等待)take()时取走一个元素 → signal notFull(叫醒等待put的生产者)