并发编程与多线程:高频问题、回答框架与公司侧重点
并发编程是Java高级开发面试的必考点,考察点涵盖锁机制、线程安全、内存模型、工具类、性能优化五大领域。面试官常通过一个具体问题同时探测你的基础原理、实践经验、异常处理三个深度层级。下面先给出通用回答框架,配合三个高频问题卡片,帮助你在面试中快速建立“原理→实现→陷阱→优化”的完整输出路径。
核心原则:任何并发问题回答都遵循“识别并发问题 → 选择合适机制 → 分析边界条件 → 验证性能与正确性 → 总结适用场景”五步法。
面试加分点:回答时主动提及“先判断共享变量是否为不可变对象”或“优先考虑无锁数据结构”,可体现工程思维。
🔹 面试问题1:synchronized 和 ReentrantLock 的区别是什么?分别适合什么场景?
🎯 面试官考察点
- 是否理解两种锁的底层实现(偏向锁 → 轻量锁 → 重量锁 vs AQS+CAS)
- 是否关注可中断性、公平性、条件变量等高级特性
- 能否结合业务场景(如高并发读多写少、长时间等待)给出选择依据
📐 回答框架
- 简述各自原理:synchronized 是 JVM 内置锁,基于 monitor 对象;ReentrantLock 基于 AQS 实现,提供更灵活的 API。
- 对比关键差异:是否可中断、是否公平、是否绑定多个 Condition、锁释放方式(自动 vs 手动)。
- 性能对比:JDK 1.6 优化后 synchronized 在低竞争下性能接近 Lock;高竞争下 Lock 可避免长时间阻塞。
- 场景推荐:简单的同步逻辑优先使用 synchronized(代码简洁不易出错);需要超时、中断、公平锁时选 Lock。
- 总结升华:选择锁时先考虑使用并发容器(如 ConcurrentHashMap)或无锁数据结构,再考虑显式锁。
💬 简短示例回答
“synchronized 是 JVM 层面内置锁,自动释放,适合简单同步;ReentrantLock 是 API 层面 AQS 实现,支持可中断、公平锁和多个 Condition。性能上 JDK1.6 后两者差距缩小。我会优先用 synchronized 保证正确性,当需要尝试锁、限时等待或更细粒度的条件等待时才会选择 ReentrantLock。” (85字)
❓ 可能追问
- synchronized 在 JDK 1.6 做了哪些优化?偏向锁撤销的性能影响?
- ReentrantLock 的 tryLock 与 lock 的区别?怎么实现可中断?
- Condition 的 await/signal 与 Object 的 wait/notify 区别?
- 如果同时需要读多写少且写操作频繁,你会怎么设计?
⚠️ 避坑提醒
- 不要只说“synchronized 是重量级锁”——必须提到 JDK 1.6 后引入锁升级机制
- 不要忽略 Lock 必须手动 unlock(尤其异常场景下放入 finally)
- 避免直接说“Lock 性能一定更好”——实际取决于竞争的激烈程度
🔹 面试问题2:ThreadLocal 的原理是什么?如何避免内存泄漏?
🎯 面试官考察点
- 是否理解 ThreadLocalMap 使用弱引用 Entry 的设计
- 是否清楚内存泄漏的根因(线程池场景下线程长期存活)
- 能否给出实际预防措施(remove 时机、自定义清理)
📐 回答框架
- 原理简述:每个 Thread 内部维护 ThreadLocalMap,key 是 ThreadLocal 弱引用,value 是实际对象。
- 弱引用设计目的:防止 ThreadLocal 对象被GC时产生外部引用链。
- 内存泄漏原因:线程池复用线程,ThreadLocalMap 的 key 被回收后 value 仍被强引用,无法释放。
- 解决方式:
- 使用完 ThreadLocal 后显式调用 remove()
- 将 ThreadLocal 设为 private static 防止外部误操作
- 避免在线程池任务中使用 ThreadLocal 并长期占用
- 总结:正确用法是在 finally 块中调用 remove,保证无论正常还是异常都能清理。
💬 简短示例回答
“ThreadLocal 通过每个线程持有的 ThreadLocalMap 存储数据,使用弱引用修饰 key。当 ThreadLocal 对象被GC时,key 变为 null,value 仍被强引用导致泄漏。我会在 finally 块中调用 remove()。另外在 Web 应用中使用 RequestContext 时特别注意线程复用场景。” (92字)
❓ 可能追问
- ThreadLocalMap 的 Hash 冲突解决方式?为什么不用红黑树?
- 子线程如何继承父线程的 ThreadLocal 变量?
- InheritableThreadLocal 的原理与局限性?
- 如果值对象本身持有外部引用,会不会产生其他泄漏?
⚠️ 避坑提醒
- 不要只说“弱引用会导致 GC 后 value 自动回收”——错误,弱引用回收的是 key,value 依然存活
- 不要忘记强调线程池 + ThreadLocal 是最大泄漏场景
- 不要把 InheritableThreadLocal 与 ThreadLocal 混淆,后者不支持父子线程传递
🔹 面试问题3:volatile 关键字的可见性和有序性是如何实现的?可以保证原子性吗?
🎯 面试官考察点
- 是否理解 Java 内存模型(JMM)的 happen-before 原则
- 是否清楚 volatile 禁止指令重排的底层机制(内存屏障)
- 是否知道 volatile 不保证原子性及其典型错误用法(如 i++)
📐 回答框架
- 可见性实现:每次读 volatile 变量都从主内存刷新,写操作立即刷回主内存,同时失效其他线程的缓存行(缓存一致性协议 MESI)。
- 有序性实现:JMM 在 volatile 读写前后插入内存屏障(LoadLoad、StoreStore、LoadStore、StoreLoad),禁止特定类型重排序。
- 原子性说明:volatile 仅保证单次读/写操作的原子性(对 long/double 类型标准化后),不保证复合操作如 i++ 的原子性。
- 典型应用场景:状态标志位(如 shutdown)、单例模式(double-checked locking 配合 volatile)、DCL 中防止指令重排。
- 总结:volatile 适合一写多读的场景,复合操作需要使用 synchronized 或 Atomic 类。
💬 简短示例回答
“volatile 基于 JMM 和内存屏障实现可见性与有序性。读操作前插入 LoadLoad 屏障,写操作后插入 StoreStore 屏障。但它不保证原子性,例如 i++ 需要 synchronized。常用场景是 DCL 单例中的 instance 变量,防止构造函数重排导致半初始化对象被读取。” (97字)
❓ 可能追问
- 什么是指令重排?实际遇到过重排导致的 Bug 吗?
- volatile 与 synchronized 的可见性实现有何不同?
- DCL 单例中如果不加 volatile 会怎样?能否通过其他方式解决?
- JSR-133 之前 volatile 的语义有什么问题?
⚠️ 避坑提醒
- 不要回答“volatile 保证原子性”——这是经典错误,面试官专门挖坑
- 不要忽略对 long/double 的普通读写非原子性问题(早期未使用 volatile 时)
- 不要说“volatile 禁止所有重排”——实际只禁止 volatile 读写与非 volatile 操作之间的部分重排序
📌 本章小结:以上三个问题覆盖了锁机制、线程本地变量、内存模型三大核心。实际面试中还会延伸至线程池(第4章)、Fork/Join、CompletableFuture 等。回答时始终以“原理 → 场景 → 陷阱 → 优化”为主线,配合具体代码验证经历,可显著提升通过率。