面试考察点
体系化认知:面试官不仅仅是想知道你用过哪些锁,更是想知道你能不能从不同维度(锁的设计思想、实现方式、竞争策略等)把锁分类讲清楚。零散背诵和体系化理解,在面试官眼里一眼就能看出来。
原理深度:考察你是否理解 synchronized 的锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),ReentrantLock 的 AQS 实现原理,以及 CAS 乐观锁的底层支持。
实战选型能力:不同的业务场景该用什么锁?为什么?能不能讲清楚 synchronized 和 ReentrantLock 的选型依据。
核心答案
Java 中的锁不是孤立的概念,而是可以从 多个维度 来分类。核心分类如下:
上图展示了 Java 锁的完整分类体系。整体可以从以下几个维度来理解:
按设计思想:悲观锁认为冲突一定发生,先加锁再操作;乐观锁认为冲突很少,先操作再检测(CAS)
按竞争策略:公平锁按排队顺序获取锁;非公平锁允许插队,性能更好
按持有者:独享锁同一时刻只能被一个线程持有;共享锁可以被多个线程同时持有
按可重入性:可重入锁允许同一个线程重复获取同一把锁,避免死锁
按锁状态:synchronized 的锁会根据竞争情况自动升级(偏向锁 → 轻量级锁 → 重量级锁)
按实现方式:synchronized 是 JVM 关键字级别实现;Lock 接口是 JDK API 级别实现
下面逐个展开。
深度解析
一、悲观锁 vs 乐观锁
这是从 设计思想 角度分的,不是具体的某个锁实现。
悲观锁:总是假设最坏情况,认为每次操作都会有冲突,所以操作前先加锁。
// synchronized 就是典型的悲观锁
synchronized (lock) {
// 先加锁,再操作
balance -= amount;
}
// 数据库的 SELECT ... FOR UPDATE 也是悲观锁
乐观锁:总是假设最好情况,认为冲突概率很低,先操作,提交时检测是否有冲突(通常用 CAS 或版本号实现)。
// CAS(Compare And Swap)就是乐观锁的典型实现
// AtomicInteger 底层就是 CAS
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 操作:预期值为 0,新值为 1
// 数据库乐观锁:version 字段
UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
对比
悲观锁
乐观锁
思想
先锁再操作,防患于未然
先操作再检测,冲突了再重试
实现
synchronized、ReentrantLock
CAS、版本号
适用
写多读少、竞争激烈
读多写少、冲突概率低
性能
加锁有开销,但保证一致性
无锁开销,冲突时重试有成本
二、公平锁 vs 非公平锁
从 获取锁的顺序 角度分。
公平锁:多个线程按申请锁的顺序排队,先到先得。不会出现 "饥饿" 现象,但吞吐量低。
非公平锁:新来的线程直接尝试插队获取锁,失败了再去排队。吞吐量高,但可能导致排队线程长时间获取不到锁。
// ReentrantLock 支持选择公平/非公平
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
// synchronized 是非公平锁(无法设置为公平锁)
ReentrantLock 默认用非公平锁。为什么?因为公平锁每次都要检查队列里有没有排队的线程,涉及线程切换开销;而非公平锁直接尝试 CAS 抢锁,抢到了就省去了一次线程切换,性能更好。生产环境绝大部分场景用非公平锁就够了。
三、独享锁(排他锁)vs 共享锁
从 能否被多个线程同时持有 角度分。
独享锁(也叫排他锁、互斥锁):同一时刻只能被一个线程持有。synchronized、ReentrantLock 都是独享锁。
共享锁:可以被多个线程同时持有。ReentrantReadWriteLock 的读锁就是共享锁——多个线程可以同时读,但写锁是独享的。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁(共享锁)—— 多个线程可同时获取
rwLock.readLock().lock();
try {
// 读取数据
} finally {
rwLock.readLock().unlock();
}
// 写锁(独享锁)—— 只有一个线程能获取,且会阻塞读锁
rwLock.writeLock().lock();
try {
// 修改数据
} finally {
rwLock.writeLock().unlock();
}
读写锁特别适合 读多写少 的场景,比如缓存、配置中心。
四、可重入锁
可重入锁 指的是同一个线程可以重复获取同一把锁,不会被自己阻塞。
// synchronized 是可重入锁
synchronized void methodA() {
methodB(); // 在 A 持有锁的情况下调用 B,能正常获取同一把锁
}
synchronized void methodB() {
// 如果不可重入,这里就死锁了
}
// ReentrantLock 也是可重入锁(名字就告诉你了)
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock(); // 同一个线程可以重复加锁,内部用计数器记录
lock.unlock();
lock.unlock(); // 需要unlock相同次数
这点非常重要。如果锁不可重入,那一个 synchronized 方法调用另一个 synchronized 方法(同一把锁)就会直接死锁。Java 里 synchronized 和 ReentrantLock 都是可重入的,所以日常开发中不用担心这个问题。
五、synchronized 的锁升级(JDK 6+)
这块是面试的高频考点。JDK 6 之后,synchronized 引入了 "锁升级" 机制,根据竞争情况从低级锁逐步升级到高级锁:
上图展示了 synchronized 锁升级的完整过程。整体分四个阶段:
无锁:对象刚创建,还没有任何线程来访问同步代码。
偏向锁:第一个线程访问时,JVM 把线程 ID 写入对象头的 Mark Word 中。之后这个线程再次进入同步块,发现 Mark Word 里的线程 ID 是自己,直接进入,连 CAS 操作都不需要。本质上是 "偏心"——锁偏向第一个获取它的线程。
轻量级锁:当第二个线程来竞争偏向锁时,偏向锁撤销,升级为轻量级锁。此时竞争的线程通过 CAS 自旋 尝试获取锁。自旋就是不停地循环尝试,不放弃 CPU 执行时间。适合锁持有时间短的场景——线程稍微等几轮就能拿到锁。
重量级锁:如果自旋了一定次数还没拿到锁,说明竞争激烈,升级为重量级锁。此时拿不到锁的线程会被 挂起(阻塞),交由操作系统调度。涉及用户态到内核态的切换,开销比较大。
关键点:锁升级是 单向 的,只能从低到高升级,不能降级(不过有特例,比如在全局安全点 STW 期间可以降级)。这个设计的核心思想是 延迟重量级锁的使用——大部分情况下锁竞争并不激烈,用轻量级的方案就够了。
六、synchronized vs ReentrantLock
这是面试中被问频率最高的一组对比,放个表格一目了然:
对比维度
synchronized
ReentrantLock
实现层面
JVM 关键字,底层用 Monitor
JDK API,基于 AQS
加锁方式
自动加锁释放
手动 lock() / unlock()
是否可中断
不可中断
可中断(lockInterruptibly())
公平性
只支持非公平
支持公平和非公平(构造参数)
条件变量
只有一个等待队列(wait/notify)
支持多个条件队列(Condition)
锁升级
支持偏向锁 → 轻量级锁 → 重量级锁
不支持,直接 CAS + AQS
适用场景
简单同步、代码块保护
需要高级功能(超时、中断、公平)
怎么选?简单的同步场景用 synchronized,代码更简洁,JVM 层面持续优化(锁升级、锁消除、锁粗化)。需要超时获取、可中断、公平锁、多条件队列这些高级功能时用 ReentrantLock。
七、自旋锁
自旋锁的核心思路就是:拿不到锁?不死等,先 "原地转圈" 试几次。
// 自旋锁的简单实现
class SpinLock {
private AtomicReference
public void lock() {
Thread current = Thread.currentThread();
// CAS 自旋:如果 owner 为 null 就设为当前线程,否则一直循环
while (!owner.compareAndSet(null, current)) {
// 自旋等待
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
好处是避免了线程挂起/恢复的内核态切换开销。坏处是如果锁被持有的时间长,自旋会白白消耗 CPU。所以适合 锁持有时间短 的场景。
八、其他常见锁一览
锁类型
说明
典型代表
读写锁
读读共享,读写互斥,写写互斥
ReentrantReadWriteLock
分段锁
把数据分段,每段独立加锁,提高并发度
ConcurrentHashMap(JDK 7 的 Segment)
死锁
两个线程互相持有对方需要的锁,永久等待
—(这是需要避免的问题)
ConcurrentHashMap 在 JDK 7 中用 Segment 分段锁,每个 Segment 是一把 ReentrantLock,默认 16 段,理论上支持 16 个线程并发写。JDK 8 改为 CAS + synchronized(对每个桶的头节点加锁),并发度从 "段数" 提升到 "桶数",粒度更细。
面试高频追问
synchronized 和 ReentrantLock 怎么选?
简单同步用 synchronized(代码简洁、JVM 持续优化),需要高级功能(可中断、超时、公平、多条件)用 ReentrantLock。
什么是 CAS?有什么问题?
Compare And Swap,乐观锁的底层实现。问题有三个:ABA 问题(用 AtomicStampedReference 解决)、自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量的原子性(用 AtomicReference 包装多个变量)。
什么是锁消除和锁粗化?
锁消除是 JIT 优化——检测到不可能被共享的对象上的 synchronized 会自动消除。锁粗化是 JIT 把多次连续的加锁解锁合并为一次,减少开销。
synchronized 锁升级能降级吗?
正常情况下不能,只升不降。但在 STW(全局安全点)期间,JVM 允许做降级优化。
常见面试变体
"说说 synchronized 和 ReentrantLock 的区别?"
"什么是 CAS?ABA 问题怎么解决?"
"synchronized 的锁升级过程是怎样的?"
"什么是公平锁?synchronized 是公平锁吗?"
记忆口诀
锁分类六大维度:思想(悲观/乐观)、策略(公平/非公平)、持有(独享/共享)、重入(可/不可)、状态(偏向→轻量→重量)、实现(关键字/API)。
总结
Java 锁的分类不是孤立的,同一个锁从不同维度看会有不同 "身份"。比如 synchronized 是悲观锁、非公平锁、独享锁、可重入锁,还支持锁升级。面试时先给出分类体系的全景图,再挑 2-3 个重点维度深入展开(推荐悲观锁/乐观锁和锁升级),面试官就知道你是有体系地掌握了。