
一、从一次惨痛的宕机说起三年前我接手过一个支付系统的维护任务上线后的第一个周末凌晨三点监控报警订单状态在并发场景下出现了“已支付”和“未支付”同时存在的脏数据。那是一个典型的秒杀场景用户点击支付后系统先扣减库存再更新订单状态最后发送消息通知。代码里只用了最简单的HashMap缓存订单状态配合一个没有同步的计数器。当1000个并发请求同时涌入HashMap直接变成了环形链表CPU飙升到100%整个应用挂死。复盘时发现写这段代码的同事并非不懂线程安全而是“觉得百万并发离自己很远”。这种心态恰恰是面试官最想击碎的。面试中讲线程安全如果只背八股文“volatile保证可见性synchronized保证原子性”面试官听完会礼貌性点头然后给你一个“缺乏深度”的评分。真正的加分项是你能把一个看似简单的线程问题用真实的生产事故把它背后的内存模型、锁升级、死锁检测讲透。下面我就用这个支付案例贯穿始终从原理到实战帮你构建一套面试官听了会眼睛发亮的回答框架。二、线程安全的三座大山原子性、可见性、有序性先回到那个宕机的根本原因。线程安全不是“加个锁就好”这么粗暴它本质上是你要同时管理好三个维度原子性操作不可分割、可见性一个线程的修改对其他线程立即可见、有序性防止指令重排带来的意外语义。那个支付案例里用了没有同步的i更新库存i在字节码层面是三条指令读-改-写线程A读到了100线程B也读到了100各自加1后写回结果库存只减了1这就是原子性失效。同时订单状态被线程A写入了“已支付”但线程B因为CPU缓存的原因看到的还是“未支付”这是可见性问题。最后JVM可能会为了性能将“更新订单”和“扣减库存”重排序导致订单已支付但库存没扣这又是有序性问题。面试官特别喜欢追问“你凭什么说synchronized就能同时解决这三个问题”正确的回答是synchronized在进入同步代码块时会强制刷新工作内存到主内存可见性退出时强制写回有序性并且互斥保证了原子性。但如果你只用volatile它只能保证可见性和有序性无法保证复合操作的原子性。很多候选人会混淆这一点面试官一旦听到“volatile可以用来做计数器”基本就判断你基础不扎实。三、用真实案例还原锁的选择困境回到支付系统我的修复方案一开始用了synchronized修饰整个扣库存方法。上线后压力测试通过但生产环境发现RT响应时间从5ms飙升到200ms。原因很简单synchronized是悲观锁无论是否冲突都会阻塞其他线程。在秒杀场景下大量线程同时争抢同一把锁导致线程频繁挂起、恢复操作系统上下文切换成了瓶颈。这就是锁粗化的负面效果。后来我改用了ConcurrentHashMap的原子性操作replace(key, oldValue, newValue) CAS自旋将RT降回了10ms以内。在面试中你要能根据场景主动选择合适的锁机制而不是只会无脑加synchronized。比如读多写少用ReadWriteLock写操作少但竞争激烈用StampedLock的乐观读复杂状态流转用Lock。举一个我实际遇到的例子一个配置中心需要实时更新但几乎99%是读请求。我用了ReadWriteLock写锁有20ms延迟但读锁完全无阻塞每秒支撑了10万次读取。面试官听完后一定会追问ReadWriteLock底层的AQS如何实现读锁共享这时你就可以顺势深入AQS的state设计高16位表示读锁计数低16位表示写锁持有线程的重入次数两个状态通过CAS原子更新读锁可同时被多个线程持有写锁只能独占。四、深入底层锁升级与偏向锁被淘汰的真相很多面试题会问“synchronized在JDK 1.6之后的优化”但99%的回答只会背诵“偏向锁→轻量级锁→重量级锁”。你如果能解释清楚为什么偏向锁在JDK 15被默认禁用面试官绝对会高看你一眼。偏向锁的核心思想假设只有一个线程频繁获取锁那就直接把锁对象的Mark Word指向该线程ID无需CAS操作。但在高并发应用中线程一旦切换就需要撤销偏向锁撤销过程需要等待全局安全点STW这在高并发系统里简直是灾难。我参与过的一个消息中间件项目就因为偏向锁的撤销导致频繁的Stop-The-WorldGC日志里看到大量“Biased Locking revocation”暂停耗时超过100ms。后来我们直接-XX:-UseBiasedLocking关闭偏向锁延迟反而稳定了。锁升级的完整路径应该是无锁→偏向锁JDK 15后默认不启用→轻量级锁CAS自旋→重量级锁OS互斥量。面试时你可以用一个计数器例子来演示当10个线程并发累加synchronized会先尝试自旋轻量级锁如果自旋超过一定次数默认10次或自适应锁会升级为重量级锁线程进入等待队列。这里有个隐藏知识点自旋会消耗CPU适合锁持有时间短的场景重量级锁适合锁持有时间长、竞争激烈的场景。你甚至可以提到“自适应自旋”JVM会根据上次自旋成功的概率动态调整自旋次数这是JVM一个很精细的工程优化。五、那些年我们踩过的死锁坑死锁是面试中的高频考点但很多候选人只会背诵“四个必要条件互斥、占有且等待、不可剥夺、循环等待”。你如果能讲一个真实发生的死锁案例并且给出排查方法面试官会认为你具备生产级问题解决能力。我遇到过的一个典型场景是线程A先锁order表再锁user表线程B先锁user表再锁order表两者同时执行时死锁。更隐蔽的是两个方法分别在不同的Service中代码审查时很难发现跨方法的锁顺序。解决死锁的常见思路有统一锁顺序所有线程按固定次序获取锁、使用tryLock带超时Lock接口的tryLock(time, unit)方法获取不到就放弃并释放已有锁、死锁检测JDK自带的jstack VisualVM。我曾经在线上用jstack导出了一次死锁现场日志里明确打印了“Found one Java-level deadlock”然后看到了三个线程互相等待的详细信息。面试官如果追问你“如何手动触发死锁”你可以写一段简单代码一个线程先锁A再锁B另一个线程先锁B再锁A然后Thread.sleep确保交叉发生最后用jstack验证。这种动手能力比任何理论都加分。六、面试必杀技用代码说话面试官问“线程安全”时最好的策略是打开IDE现场写一段代码边写边解释。比如我经常演示一个“简单的计数器”public class UnsafeCounter { private int count 0; public void increment() { count; } // 问题count不是原子操作 }然后我改成两种方案方案A用synchronized方案B用AtomicInteger。我会指出AtomicInteger底层是Unsafe的compareAndSwapInt通过硬件级别的CAS指令保证原子性没有锁切换的开销但可能出现ABA问题。提到ABA就可以引出AtomicStampedReference的版本号机制。如果你再提到JMH基准测试证明在低竞争下AtomicInteger比synchronized快20倍但在高竞争下synchronized由于锁升级后的阻塞反而更省CPU面试官绝对会刮目相看。另一个面试高发区是ThreadLocal的内存泄漏。我曾遇到一个Web应用每次请求都创建一个ThreadLocal存储用户信息但线程池中的线程复用后ThreadLocal的value没有被remove导致上一次请求的用户信息泄漏给下一次请求。这就是为什么ThreadLocal用完一定要调用remove()特别是在线程池环境中。更深入一点ThreadLocalMap的Entry是弱引用如果Key被GC回收value会永远无法被访问形成内存泄漏。你可以主动告诉面试官在阿里巴巴Java开发手册中明确规定“必须回收自定义的ThreadLocal变量”。七、AQS并发工具的基石如果你能讲清楚AQSAbstractQueuedSynchronizer面试基本稳了。ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的。AQS的核心是一个volatile int state和一个CLHFIFO等待队列。以ReentrantLock为例state为0表示无锁1表示有人持有锁大于1表示重入次数。非公平锁的实现tryAcquire直接CAS抢锁抢不到就进队列公平锁则需判断前驱节点是否在队列头部。面试官很喜欢问“你平时用ReentrantLock能说说它和synchronized的区别吗”标准回答是synchronized是隐式锁由JVM管理支持锁升级ReentrantLock提供了可中断、可超时、多个Condition条件等高级功能。但你要加上一句在性能上JDK 1.6之后两者在低竞争场景下几乎无差别高竞争场景下ReentrantLock的灵活性更高。八、面试中必须避开的陷阱有些所谓的“线程安全”做法其实是反模式。比如用Hashtable而不考虑性能或者用String作为锁对象。String常量池会带来可怕的副作用当两个地方用了内容相同的String作为锁它们实际上是同一把锁会导致完全不相关的代码块互相同步。正确做法是new Object()或专门的锁对象。另一个陷阱是double-checked locking双重检查锁单例模式不加volatile时由于指令重排第一个线程new Instance()时可能先赋值了引用对象还没初始化第二个线程读到非null引用却使用了一个半构造的对象。这就是为什么单例里instance必须加volatile。面试官如果听到你把这个场景讲清楚会认为你彻底理解了happens-before原则。九、从线程安全到系统设计面试最后面试官往往会问“如果让你设计一个高并发的库存扣减系统你怎么保证线程安全”一个好的回答不是“加锁”而是结合业务场景分层设计。比如用Redis原子自减做库存扣减因为Redis是单线程模型天然线程安全扣减成功后异步发消息落到MySQL做最终一致性。这叫做“用系统架构规避线程安全问题”。而Java层面的原子类只适合做本地计数器。如果你还能提到乐观锁的ABA问题在金额扣减场景下特别危险——因为ABA可能导致余额凭空多出一倍所以必须用版本号或时间戳——面试官会更加认可你的工程思维。最后送你一句我认为面试线程安全时最加分的金句“线程安全不是代码问题而是数据共享策略问题。如果数据根本不共享那就没有线程安全问题。”这句话直指本质能让面试官觉得你的思考已经超越了语法层面。现在你准备好了吗下次面试把这些案例和原理融进你的回答里你会看到面试官的眼睛在发光。