一、为什么面试官总爱问“锁策略”多线程并发编程一直是 Java 后端面试的高频考点而在并发编程中“锁”又是绕不开的核心主题。很多同学能背出 synchronized、ReentrantLock、CAS、乐观锁、悲观锁这些名词但一旦面试官追问“你为什么选择公平锁而不是非公平锁”“偏向锁到底解决了什么问题”“StampedLock 和 ReentrantReadWriteLock 的区别是什么”往往就答得支离破碎。根本原因在于大家在学习锁的时候更多是在记忆 API而没有形成一套完整的“锁策略”视角。所谓锁策略并不是某一个具体的类或关键字而是一组解决并发安全问题的设计思想和取舍原则。同一个锁实现可能同时具备多种策略特征例如 ReentrantLock 既可以是非公平锁也可以是公平锁既可以作为互斥锁使用也可以通过 Condition 实现等待唤醒。这篇文章会从面试官视角出发系统梳理多线程中常见的锁策略包括乐观锁与悲观锁、公平锁与非公平锁、独占锁与共享锁、可重入锁、自旋锁、偏向锁、轻量级锁、重量级锁、分段锁、锁消除与锁粗化、死锁与活锁等内容并结合 synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock、AQS、CAS 等具体实现进行深入分析。全文约两万字建议收藏后分章节阅读。阅读提示如果你准备的是初中级岗位重点掌握第一节到第十一节如果目标是高级或专家岗位请特别关注 AQS 源码思路、锁升级流程、StampedLock 读写模式以及实际项目中的锁选型。二、锁策略的总览框架在深入具体策略之前我们先建立一张总体知识地图。多线程中的常用锁策略可以从以下几个维度进行划分分类维度策略名称核心思想典型实现是否假设竞争乐观锁 / 悲观锁悲观锁认为冲突一定发生乐观锁认为冲突不一定发生synchronized / CAS是否排队公平公平锁 / 非公平锁公平锁按等待顺序获取非公平锁允许插队ReentrantLock允许多少线程同时访问独占锁 / 共享锁独占锁一次只允许一个线程共享锁允许多个读线程ReentrantReadWriteLock同一线程能否重复获取可重入锁 / 不可重入锁可重入锁允许同一线程多次获取同一把锁synchronized、ReentrantLock获取失败后是否等待自旋锁 / 阻塞锁自旋锁反复尝试阻塞锁让出 CPUAtomicInteger、AQS是否按对象状态优化偏向锁 / 轻量级锁 / 重量级锁根据竞争情况逐步升级锁状态synchronized是否拆分锁粒度分段锁 / 细粒度锁把一个全局锁拆成多个小锁减少竞争ConcurrentHashMap下面我们逐一拆解每种策略的原理、优缺点和使用场景。三、乐观锁与悲观锁3.1 什么是悲观锁悲观锁的核心假设是共享资源每次被访问时都大概率会发生冲突所以必须先加锁再操作数据。它的思路非常直接就像一个人总是往坏处想只要我不锁起来别人一定会来改。因此悲观锁在操作数据前会先通过锁机制把资源保护起来其他线程只能阻塞等待。在 Java 中典型的悲观锁实现包括 synchronized 关键字和各种 Lock 接口实现。例如public class PessimisticLockDemo { private int count 0; private final Object lock new Object(); public void increment() { synchronized (lock) { count; } } }悲观锁的优点是实现简单、语义清晰能够保证线程安全。缺点也很明显无论是否真的发生冲突每次都要付出加锁和解锁的代价在高并发场景下如果竞争激烈会导致大量线程阻塞和上下文切换性能下降。此外悲观锁如果使用不当还容易引发死锁问题。3.2 什么是乐观锁乐观锁的核心假设是共享资源大多数情况下不会发生冲突所以先不上锁直接操作在更新时再检查是否有其他线程修改过数据。如果发现数据已经被修改就放弃本次更新或进行重试如果没有被修改则提交更新。乐观锁最经典的实现机制是CASCompare And Swap比较并交换。CAS 包含三个操作数内存位置 V、期望原值 A、新值 B。只有当 V 的值等于 A 时才把 V 更新为 B否则不做任何操作并返回失败信息。Java 中的原子类就是基于 CAS 实现的import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockDemo { private AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); } }乐观锁的优点是在冲突较少的场景下性能很高因为不需要加锁和阻塞线程。缺点是一旦冲突频繁CAS 会反复失败并重试浪费 CPU 资源。此外乐观锁还存在著名的ABA 问题我们稍后详细讨论。3.3 乐观锁与悲观锁的对比对比项悲观锁乐观锁核心假设冲突一定会发生冲突不一定会发生加锁时机操作数据前加锁操作数据时不加锁更新时校验实现方式synchronized、LockCAS、版本号机制线程阻塞可能阻塞一般不阻塞失败重试适用场景写多读少、冲突激烈读多写少、冲突较少潜在风险死锁、性能下降ABA 问题、CPU 空转实际开发中并不能简单地说乐观锁一定比悲观锁好。如果竞争非常激烈CAS 反复失败会导致 CPU 长时间空转此时悲观锁反而更合适。选择锁策略一定要结合具体业务场景和并发压力进行压测。四、公平锁与非公平锁4.1 基本概念公平锁和非公平锁描述的是多个线程在等待同一把锁时的获取顺序。公平锁多个线程按照申请锁的先后顺序先到先得。新来的线程如果发现锁已经被占用会进入等待队列排队不会插队。非公平锁多个线程获取锁的顺序不一定是先到先得。新来的线程在尝试获取锁时如果锁刚好被释放可以直接抢占不必排队。ReentrantLock 提供了公平锁和非公平锁两种实现。默认构造方法创建的是非公平锁传入 true 则创建公平锁import java.util.concurrent.locks.ReentrantLock; public class FairLockDemo { // 非公平锁 private final ReentrantLock nonFairLock new ReentrantLock(); // 公平锁 private final ReentrantLock fairLock new ReentrantLock(true); }synchronized 在 Java 层面只能使用非公平锁无法指定为公平锁。这是因为 synchronized 依赖 JVM 底层的监视器机制而该机制并未提供公平性保证。4.2 公平锁与非公平锁的优缺点公平锁的优点是每个线程都能得到执行机会不会出现某些线程长期等待甚至“饥饿”的情况尤其在任务执行时间差异较大的场景下更加友好。公平锁的缺点是性能较低。为了保证顺序公平锁需要维护严格的等待队列并且每次线程切换都伴随大量上下文切换。很多锁在释放的瞬间等待队列中的线程还没来得及被唤醒如果此时强行要求公平就会白白浪费一次加锁机会。非公平锁的优点是吞吐量更高。因为新线程有机会直接获取刚释放的锁减少了线程阻塞和唤醒的次数提升了整体执行效率。非公平锁的缺点是可能造成线程饥饿。某些优先级较低或运气较差的线程可能长时间拿不到锁不过在实际应用中长时间饥饿的情况相对较少。4.3 ReentrantLock 公平锁与非公平锁的源码体现在 ReentrantLock 中公平锁和非公平锁的核心差异主要体现在 tryAcquire 方法。公平锁会先判断等待队列中是否有前驱节点只有队列为空或当前线程是队首时才能尝试获取锁// 公平锁的 tryAcquire 简化逻辑 protected final boolean tryAcquire(int acquires) { Thread current Thread.currentThread(); int c getState(); if (c 0) { if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { // 可重入逻辑 int nextc c acquires; setState(nextc); return true; } return false; }关键就在于hasQueuedPredecessors()它会检查当前线程前面是否还有等待节点。非公平锁则直接尝试 CAS 抢占不会进行这层检查。4.4 面试常见追问问为什么默认使用非公平锁因为非公平锁在多数场景下吞吐量更高。线程被唤醒本身就存在时间差非公平锁可以利用这个时间差让新线程快速完成加锁与释放避免 CPU 空转。问公平锁就真的完全公平吗公平锁只是尽量保证按队列顺序获取但并不能保证绝对公平。线程调度由操作系统控制一个线程即使拿到了锁的执行资格也可能因为 CPU 时间片轮转而被暂停。五、独占锁与共享锁5.1 概念解析独占锁和共享锁描述的是同一时刻允许多少个线程持有锁。独占锁也常被称为互斥锁、排他锁或写锁。同一时刻只允许一个线程持有该锁其他线程必须等待。共享锁也常被称为读锁。同一时刻允许多个线程同时持有该锁但这些线程通常只能进行读操作不能修改数据。Java 中的 synchronized 和 ReentrantLock 都是独占锁。而 ReentrantReadWriteLock 则把读写操作区分开内部维护了一把读锁和一把写锁实现了读写分离。import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockDemo { private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock rwLock.writeLock(); private int data; public int read() { readLock.lock(); try { return data; } finally { readLock.unlock(); } } public void write(int value) { writeLock.lock(); try { data value; } finally { writeLock.unlock(); } } }5.2 读写锁的核心价值在很多业务场景中读操作远远多于写操作而且读操作之间并不需要互斥。如果所有操作都使用独占锁那么多个读线程也必须串行执行严重浪费并发能力。引入共享锁后多个读线程可以同时读取数据只有写线程才需要独占访问从而大幅提升读多写少场景下的吞吐量。不过读写锁也带来了新的问题读写互斥、写写互斥的规则使得锁的调度更加复杂。特别是在写线程不断到达时读线程可能一直无法获取读锁出现读线程饥饿问题。5.3 读写锁的互斥规则当前持有锁请求读锁请求写锁无锁允许允许读锁允许不允许写锁不允许不允许简单记忆读读可以共存读写互斥写写互斥。需要注意的是在 ReentrantReadWriteLock 的默认实现中如果已经有读线程持有读锁此时写线程请求写锁需要等待同时如果写线程已经在排队后续到达的读线程是否还能获取读锁取决于锁的公平性设置。六、可重入锁与不可重入锁6.1 什么是可重入锁可重入锁也叫递归锁指的是同一个线程在持有锁的情况下可以再次获取同一把锁而不会被自己阻塞。如果锁不可重入线程在递归调用或嵌套调用加锁方法时会因为无法再次获得已经被自己持有的锁而死锁。Java 中的 synchronized 和 ReentrantLock 都是可重入锁。synchronized 的可重入性由 JVM 内部实现每次重入时监视器的重入次数加一退出时减一次数归零时才会真正释放锁。public class ReentrantDemo { public synchronized void methodA() { System.out.println(methodA start); methodB(); } public synchronized void methodB() { System.out.println(methodB start); } }在上面的代码中方法 methodA 和 methodB 都被 synchronized 修饰。当线程调用 methodA 后又在其内部调用 methodB因为 synchronized 是可重入锁所以 methodB 能够顺利获取同一把对象锁而不会发生死锁。6.2 ReentrantLock 的可重入原理ReentrantLock 内部通过一个 state 变量记录重入次数。每次成功加锁state 加一每次解锁state 减一。当 state 归零时才真正释放锁并唤醒等待队列中的线程。// ReentrantLock 非公平锁 tryAcquire 的简化实现 final boolean nonfairTryAcquire(int acquires) { Thread current Thread.currentThread(); int c getState(); if (c 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) { throw new Error(Maximum lock count exceeded); } setState(nextc); return true; } return false; }一旦当前线程已经是锁的持有者就会直接累加 state 而不会发生阻塞这就是可重入的具体体现。6.3 不可重入锁有什么问题不可重入锁在获得锁之后如果当前线程再次尝试获取同一把锁就会自己阻塞自己最终导致死锁。public class NonReentrantLock { private boolean locked false; public synchronized void lock() { while (locked) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } locked true; } public synchronized void unlock() { locked false; notify(); } }在上面的实现中lock()方法已经用synchronized修饰同时内部又通过wait()让竞争线程等待。如果当前线程已经获得锁又在同一线程内再次调用lock()由于locked已经被置为true线程会进入while (locked)的等待分支自己把自己阻塞住而释放锁又需要同一个线程继续执行到unlock()于是形成死锁。这也解释了为什么 synchronized 和 ReentrantLock 会专门设计可重入能力在实际项目中递归调用、嵌套调用、模板方法等场景非常普遍如果锁不可重入代码很容易在无意中自锁。七、自旋锁与阻塞锁7.1 什么是自旋锁自旋锁描述的是线程获取锁失败后不立即挂起休眠而是在原地循环尝试直到成功获取锁为止。它的核心思想是如果锁很快就会被释放与其让线程进行昂贵的上下文切换不如让 CPU 忙等一小段时间。在 Java 中自旋锁通常依赖 CAS 实现。下面的例子通过 AtomicReference 模拟一个简单的自旋锁import java.util.concurrent.atomic.AtomicReference; public class SimpleSpinLock { private final AtomicReferenceThread owner new AtomicReference(); public void lock() { Thread current Thread.currentThread(); while (!owner.compareAndSet(null, current)) { // 自旋等待不做任何事 } } public void unlock() { Thread current Thread.currentThread(); owner.compareAndSet(current, null); } }7.2 自旋锁的优缺点优点是避免了线程阻塞和唤醒带来的上下文切换开销。如果锁的持有时间非常短自旋等待的代价远小于两次线程切换的代价。缺点是如果锁被长时间持有自旋会白白占用 CPU甚至出现多个线程一起空转导致系统吞吐量下降。因此自旋锁适合锁竞争不激烈、临界区非常短的场景。7.3 阻塞锁与自适应自旋锁与自旋锁相对的是阻塞锁线程获取锁失败后主动挂起让出 CPU当锁释放后再被唤醒。阻塞锁能避免 CPU 空转但线程切换成本更高。JVM 对 synchronized 做了自适应自旋优化如果同一次自旋曾经成功获取过锁下一次就适当增加自旋次数如果自旋很少成功就减少甚至直接阻塞从而在自旋和阻塞之间动态平衡。八、偏向锁、轻量级锁与重量级锁8.1 锁升级的核心思想偏向锁、轻量级锁、重量级锁是 synchronized 在 JVM 层面的锁升级过程。JVM 会根据竞争激烈程度逐步把锁从低开销状态升级到高开销状态目的是在竞争不激烈时尽量少付出锁开销在竞争激烈时保证线程安全。锁的升级顺序一般是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这些状态记录在对象头的 Mark Word 中。8.2 偏向锁偏向锁假设锁总会被同一个线程获取。当某个线程第一次获取锁时JVM 会把锁“偏向”给这个线程并在对象头中记录线程 ID以后该线程再次进入同步块时不需要任何 CAS 操作直接判断 Mark Word 即可开销极低。偏向锁适合只有一个线程反复获取同一把锁的场景。一旦其他线程也来竞争偏向锁会撤销升级为轻量级锁。需要注意的是JDK 15 开始默认禁用偏向锁因为现代应用中的竞争模式已经发生变化偏向锁的维护成本有时反而更高。8.3 轻量级锁当多个线程竞争但冲突不激烈时JVM 使用轻量级锁。线程会通过 CAS 把 Mark Word 替换成指向栈中锁记录的指针失败则进入自旋等待。轻量级锁避免了操作系统层面的互斥量操作减少了线程阻塞。8.4 重量级锁当竞争非常激烈、自旋长时间无法成功时锁会升级为重量级锁。重量级锁依赖操作系统的 monitor 互斥量竞争失败会被阻塞挂起涉及用户态和内核态的切换开销最大但 CPU 消耗小。8.5 升级与降级一般情况下锁只能从低级向高级升级。重量级锁释放后对象会回到无锁状态而不是逐步降回偏向锁。理解这个过程有助于解释“为什么 synchronized 后来性能优化了”——它不再是简单的一把重量级锁而是能根据场景动态调整策略。九、分段锁9.1 什么是分段锁分段锁的核心思想是把一个全局锁拆分成多个小锁不同部分的数据由不同锁保护从而减少锁竞争。最经典的例子就是 JDK 7 的 ConcurrentHashMap。JDK 7 把哈希表分成多个 Segment每个 Segment 独立加锁写入不同 Segment 的数据可以并行进行整体锁粒度从“整个表”降为“某一段”。9.2 分段锁的演进JDK 8 对 ConcurrentHashMap 做了重构不再使用 Segment而是改用CAS synchronized插入时通过 CAS 初始化数组节点发生哈希冲突时只对桶的首节点加锁。这样锁的粒度进一步细化为“单个桶”并减少了分段设计带来的复杂度。9.3 分段锁的优缺点优点是在高并发场景下显著降低竞争提高吞吐量缺点是当需要执行跨多段的全局操作时必须同时获取多把锁实现复杂且容易出错。分段锁适合“大部分操作只影响局部数据”的场景。十、锁消除与锁粗化10.1 锁消除锁消除是 JVM 在 JIT 编译阶段的一项优化。编译器通过逃逸分析判断某个对象是否可能被多个线程访问如果对象只会被当前线程使用那么对这个对象加的锁完全可以去掉。典型例子是在方法内部使用 StringBuffer 或 Vector虽然它们的每个方法都有 synchronized但由于对象不会逃逸到其他线程JVM 会直接消除锁避免无意义的同步开销。10.2 锁粗化锁粗化与锁消除相反它是把多次细粒度的加锁解锁合并为一次更大的锁。例如在一个循环里反复对同一把锁加锁、解锁JVM 可能把锁的粒度扩大到整个循环外部减少加锁次数// 原始代码循环内频繁加锁 public void append(StringBuilder sb, String... values) { for (String value : values) { synchronized (this) { sb.append(value); } } } // 锁粗化后的等价语义整个循环只需要一把锁 public synchronized void append(StringBuilder sb, String... values) { for (String value : values) { sb.append(value); } }需要注意的是锁粗化虽然是 JVM 的自动优化但我们在写代码时也应避免在循环内反复加锁从源头减少不必要的同步。十一、死锁与活锁11.1 死锁的四个必要条件产生死锁必须同时满足四个条件互斥条件资源一次只能被一个线程占用。占有且等待线程已经占有一个资源同时还在等待另一个资源。不可剥夺线程已获得的资源在未用完前不能被强制抢占。循环等待存在一条线程与资源的循环等待链。只要破坏其中任意一个条件就可以避免死锁。11.2 经典的死锁示例public class DeadlockDemo { private final Object lockA new Object(); private final Object lockB new Object(); public void method1() { synchronized (lockA) { synchronized (lockB) { System.out.println(method1); } } } public void method2() { synchronized (lockB) { synchronized (lockA) { System.out.println(method2); } } } }线程 T1 持有 lockA 等待 lockB线程 T2 持有 lockB 等待 lockA双方都无法继续执行形成死锁。11.3 如何排查与预防死锁排查死锁可以借助 JDK 自带的工具例如使用jstack -l 进程号查看线程状态JVM 通常会在输出中明确指出发生死锁的线程和锁信息也可以通过 JConsole、VisualVM 或 ThreadMXBean 进行检测。预防死锁的常见方式包括多个锁的获取顺序保持一致破坏循环等待。尽量使用 tryLock 并设置超时时间避免无限等待。减少锁持有时间缩小临界区。能使用一把锁完成时不要嵌套使用多把锁。11.4 活锁与饥饿活锁指的是线程虽然没有阻塞但一直在重复执行无意义的动作导致整体无法推进。典型例子是线程 A 和线程 B 互相谦让锁A 释放后 B 也同时释放结果谁都没拿到锁。饥饿指的是某些线程因为优先级低或调度策略问题始终得不到执行机会。公平锁可以减少饥饿但也可能牺牲部分吞吐量。解决活锁通常需要引入随机退避或让线程的等待时间错开。十二、总结与面试清单12.1 锁策略速查思考维度你要回答的关键点竞争是否必然悲观锁先加锁乐观锁用 CAS 或版本号后校验获取顺序是否公平公平锁先到先得非公平锁允许插队吞吐量更高同时允许多少线程独占锁单线程共享锁多读单写是否可重入synchronized、ReentrantLock 都可重入用 state 计数失败后是否等待自旋锁忙等阻塞锁挂起让出 CPUsynchronized 如何优化偏向锁、轻量级锁、重量级锁逐步升级锁粒度如何控制分段锁、细粒度锁、锁消除和锁粗化12.2 面试高频追问清单synchronized 和 ReentrantLock 在锁策略上有哪些异同可重入锁为什么不会把自己锁死它的底层 state 是怎么变化的自旋锁适合什么场景为什么不能无限制自旋偏向锁、轻量级锁、重量级锁分别在什么条件下触发ConcurrentHashMap 的锁粒度是如何演进的如何排查线上死锁如何避免活锁和饥饿12.3 最后建议学习锁策略不要背结论而要抓住一条主线每种锁策略本质上都是在“线程安全”和“并发性能”之间做取舍。回答面试题时先讲清策略的核心思想再结合具体实现、适用场景和可能的风险最后补一个实际项目中的选型例子会比单纯背诵概念更有说服力。进阶方向如果你对锁的底层实现感兴趣可以进一步阅读 AQS 的 acquire / release 源码、ReentrantReadWriteLock 的读写状态位设计以及 StampedLock 的乐观读模式并与本文的锁策略框架对照理解。