要彻底拿下 Java 锁的面试,关键不是背概念,而是能用一条主线把所有锁串起来,并瞬间讲出它们的区别和演进逻辑。下面我用老练的视角,给你一套"锁的全景图"。
一、为什么要锁?------ 可见性、原子性、有序性
并发编程三大问题:
- 可见性:线程 A 修改了变量,线程 B 看不见。
- 原子性 :
i++实际是三个指令,线程切换导致数据错乱。 - 有序性:指令重排导致意想不到的结果。
锁要解决的就是这三个问题。
二、Java 锁体系的核心演进主线(从无到有,从重到轻)
第一代:synchronized(JVM 内置,自动档)
java
synchronized (obj) { ... }
public synchronized void method() { ... }
- 底层 :对象头 MarkWord +
monitorenter/monitorexit指令。 - 升级过程(锁膨胀) :
偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量)- 偏向锁:只有一个线程反复获取,直接标记线程 ID,无竞争。
- 轻量级锁:少量线程交替执行,通过 CAS 在栈帧里记录锁记录,自旋等待。
- 重量级锁:自旋失败或竞争激烈,膨胀为 OS 互斥量,线程挂起。
- 特点:非公平、可重入、不可中断、自动释放。
- 缺点:功能单一,无法尝试加锁、无法超时、只能随机唤醒。
第二代:Lock 接口 + AQS(手动档,更灵活)
java
Lock lock = new ReentrantLock();
lock.lock();
try { ... } finally { lock.unlock(); }
- 核心基石 AQS :
AbstractQueuedSynchronizer,用 volatile state + CLH 队列 实现。- state:0 无锁,1 有锁,>1 可重入计数。
- 队列:FIFO 双向链表,存放等待线程。
- 比 synchronized 多了 :
- 可中断
lockInterruptibly() - 可超时
tryLock(time) - 公平锁
new ReentrantLock(true) - 多条件
Condition精准唤醒
- 可中断
面试区分点 :synchronized 是语言级隐式锁,Lock 是 API 级显式锁,AQS 是其灵魂。
第三代:JUC 全家桶(场景化工具)
基于 AQS 和 CAS 进一步封装:
| 工具 | 场景 | 关键区分 |
|---|---|---|
| ReentrantReadWriteLock | 读多写少 | 读读共享,读写互斥,写写互斥 |
| StampedLock | 读多写少,更高性能 | 乐观读(无锁校验),悲观读,写,不可重入! |
| CountDownLatch | 一个线程等 N 个线程 | 一次性,计数到零不可重置 |
| CyclicBarrier | N 个线程互相等待 | 可重用,到达屏障后可选执行 Runnable |
| Semaphore | 限流 | 许可数量,acquire() / release() |
| Phaser | 多阶段同步 | 替代 CyclicBarrier,支持动态增减 |
三、从不同维度分类锁(面试时瞬间区分)
1. 乐观锁 vs 悲观锁
- 悲观锁 :认为每次操作都会冲突,先加锁再操作。
synchronized、ReentrantLock、数据库select ... for update。 - 乐观锁 :认为不会冲突,直接操作,提交时检查。
CAS(AtomicInteger)、版本号(数据库update ... where version=?)、StampedLock 的乐观读。
2. 公平锁 vs 非公平锁
- 公平锁 :严格按等待队列顺序,
new ReentrantLock(true),吞吐量低,减少饥饿。 - 非公平锁:允许插队(先 CAS 抢一次),默认,吞吐量高。
synchronized只有非公平。
3. 可重入锁 vs 不可重入锁
- 可重入 :同一线程再次获取同一把锁,state 加 1,不会死锁。
synchronized、ReentrantLock、ReentrantReadWriteLock都是。 - 不可重入 :
StampedLock,重复获取会死锁,必须注意。
4. 共享锁 vs 排他锁
- 排他锁(写锁) :同一时刻只有一个线程持有。
synchronized、ReentrantLock.writeLock()。 - 共享锁(读锁) :多个线程可同时持有读锁,但写锁排他。
ReentrantReadWriteLock.readLock()。
5. 自旋锁 vs 适应性自旋锁
- 轻量级锁自旋是固定次数,JDK6 后自适应:如果上次自旋成功了,这次允许更久。
- 显式自旋锁 :JUC 里的
ReentrantLock底层也用 CAS 自旋,但失败后挂起。
四、锁优化实战(老手调试方向)
- 锁粗化 :连续多个
synchronized块合并为一个。 - 锁消除:JIT 判定不可能逃逸的变量,直接去掉锁。
- 减少锁持有时间:只锁必要的代码块,抛给异步。
- 锁分离 :
LinkedBlockingQueue的 put 和 take 不同锁。 - 无锁化 :用
Atomic类、LongAdder(高并发下分段累加)、ThreadLocal。 - 正确关闭锁 :
finally中 unlock,避免死锁。
五、死锁排查与预防
- 四条件:互斥、占有且等待、不可抢占、循环等待。
- 排查 :
jstack pid-> Found one Java-level deadlock。 - 预防 :按统一顺序加锁、
tryLock(time)、定时检测。
六、面试时"一口清"串联话术
"Java 的锁,我理解是从语言级锁发展到工具级锁的演进。
最初
synchronized解决了原子性和可见性,从偏向锁到重量级锁自动膨胀,但灵活性差。后来 JUC 提供了基于 AQS 的
Lock体系,核心就是一个 volatile 的 state 和 CLH 等待队列,能实现可中断、可超时、公平锁和 Condition 精准唤醒。再往上,
ReentrantReadWriteLock实现了读共享写互斥,StampedLock更进一步提供了乐观读,但不可重入。其他像
CountDownLatch、CyclicBarrier、Semaphore都是基于 AQS 的同步工具。从分类角度看,我清楚乐观锁(CAS/版本号)与悲观锁(synchronized/ReentrantLock)、公平与非公平、可重入与不可重入、共享与排他的区别。
生产上我会根据读多写少选
StampedLock,高并发计数用LongAdder,必须公平才开公平锁,并时刻注意死锁预防和finally释放。"
这样一套下来,面试官会觉得你对 Java 锁是融会贯通,而不是零散记忆。
要把 Java 锁这道面试大题答出碾压级的效果,不能光背概念,得用**"一条主线演进 + 场景案例 + 对比区分"**的方式讲。下面我按这个思路给你全拆一遍,每个关键知识点都配上能直接跑的代码。
1. 为什么需要锁?------ 可见性、原子性、有序性
案例:原子性缺失的悲剧
java
public class AtomicityDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) count++; });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) count++; });
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(count); // 期望20000,实际 <20000
}
}
count++ 不是原子操作,锁就是要保证这类复合操作的不可分割性。同时 volatile 只能保证可见性,管不了原子性。
2. synchronized ------ JVM 内置的自动挡锁
2.1 用法三连
java
// 1. 修饰实例方法:锁住 this
public synchronized void instanceMethod() { }
// 2. 修饰静态方法:锁住 Class 对象
public static synchronized void staticMethod() { }
// 3. 同步代码块:指定锁对象
private final Object lock = new Object();
public void block() {
synchronized (lock) {
// 业务
}
}
2.2 锁膨胀过程(从轻到重)
偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(OS 互斥量)
- 偏向锁:只有线程 A 反复进入,MarkWord 直接记 A 的线程 ID,加锁解锁只需一次 CAS。
- 轻量级锁:线程 B 来了,撤销偏向,A 在栈帧里留 Lock Record,B 自旋等待。
- 重量级锁:自旋超过阈值或竞争太烈,膨胀为操作系统互斥量,未抢到的线程进入阻塞队列。
案例:验证偏向锁延迟 (JVM 启动后偏向锁有 4 秒延迟,加参数 -XX:BiasedLockingStartupDelay=0 立刻开启)。
2.3 可重入验证
java
public synchronized void methodA() {
System.out.println("A");
methodB(); // 同一线程重入,不会死锁
}
public synchronized void methodB() {
System.out.println("B");
}
synchronized 天然可重入,JVM 用计数器 recursions 记录。
3. Lock 接口与 AQS ------ 手动挡高性能锁
3.1 AQS 核心:volatile state + CLH 队列
java
// AQS 骨架
public abstract class AbstractQueuedSynchronizer {
private volatile int state; // 0 无锁,1 有锁,>1 重入计数
// CLH 变体队列,双向链表存放等待线程
}
ReentrantLock 就是通过 CAS 修改 state 抢锁,失败入队。
3.2 ReentrantLock 标准用法
java
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}
}
3.3 可中断、可超时
java
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try { ... } finally { lock.unlock(); }
} else {
// 超时处理
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
3.4 公平锁 vs 非公平锁
java
ReentrantLock fairLock = new ReentrantLock(true); // 公平
ReentrantLock unfairLock = new ReentrantLock(); // 非公平
- 非公平:上来先 CAS 抢一次,失败再排队。吞吐高。
- 公平:严格 FIFO。减少饥饿。
3.5 Condition 精准唤醒(生产者消费者)
java
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(Object obj) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 释放锁,等待 notFull
}
queue.add(obj);
notEmpty.signal(); // 唤醒一个消费者
} finally {
lock.unlock();
}
}
Object.wait/notify 只能随机唤醒,Condition 可以分组控制。
4. 读写锁与 StampedLock ------ 读多写少的极致优化
4.1 ReentrantReadWriteLock 缓存案例
java
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object val) {
rwLock.writeLock().lock();
try {
cache.put(key, val);
} finally {
rwLock.writeLock().unlock();
}
}
规则:读读共享,读写互斥,写写互斥。适合读多写少。
4.2 StampedLock 乐观读,性能更高但不可重入
java
private final StampedLock sl = new StampedLock();
private double x, y;
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 乐观读
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 校验版本
stamp = sl.readLock(); // 升级为悲观读锁
try {
currentX = x; currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}
注意 :StampedLock 不可重入 ,重复获取会死锁。它没有自动释放,容易出错,但性能比 ReadWriteLock 更好。
5. JUC 同步工具案例
5.1 CountDownLatch ------ 一等多,一次性
java
int n = 5;
CountDownLatch latch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
new Thread(() -> {
doWork();
latch.countDown();
}).start();
}
latch.await(); // 主线程等待所有子线程完成
System.out.println("全部完成");
5.2 CyclicBarrier ------ 互相等,可重复
java
int parties = 4;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> System.out.println("所有线程到达,开始下一轮"));
for (int i = 0; i < parties; i++) {
new Thread(() -> {
while (!finished) {
doPart();
barrier.await(); // 互相等待
}
}).start();
}
区别 :Latch 是等待其他线程做减法,Barrier 是线程之间互相阻塞等齐。Latch 一次性,Barrier 可重置。
5.3 Semaphore ------ 限流
java
Semaphore semaphore = new Semaphore(5); // 最多5个并发
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
semaphore.acquire();
doLimitedWork();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}).start();
}
5.4 Phaser ------ 多阶段同步
java
Phaser phaser = new Phaser(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int phase = 0; phase < 3; phase++) {
doPhase(phase);
phaser.arriveAndAwaitAdvance();
}
}).start();
}
6. 锁的分类与对比,案例区分
| 维度 | 类型 | 代表 | 案例 |
|---|---|---|---|
| 乐观/悲观 | 悲观锁 | synchronized, ReentrantLock | 银行转账:synchronized(account) 防止并发扣款 |
| 乐观锁 | CAS, AtomicInteger, 版本号 | 扣库存:update product set stock=stock-? where id=? and stock>=? |
|
| 公平/非公平 | 公平锁 | new ReentrantLock(true) |
需要顺序执行的排队业务 |
| 非公平锁 | new ReentrantLock(), synchronized |
高吞吐通用场景 | |
| 可重入/不可重入 | 可重入 | synchronized, ReentrantLock | 递归调用不会死锁 |
| 不可重入 | StampedLock | 重复 readLock() 直接卡死 |
|
| 共享/排他 | 共享锁 | 读锁 (ReadLock) |
多线程同时读配置缓存 |
| 排他锁 | 写锁 (WriteLock), synchronized |
写操作必须排他 | |
| 自旋/阻塞 | 自旋锁 | 轻量级锁阶段,AtomicInteger |
短时间等待,自旋避免上下文切换 |
| 阻塞锁 | 重量级锁 | 长时间等待,让出 CPU |
乐观锁的 CAS 案例:
java
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // CAS 自旋直到成功
数据库乐观锁案例:
sql
UPDATE orders SET status = 'PAID' WHERE id = ? AND status = 'UNPAID';
返回影响行数为 0 表示已支付,防止重复扣款。
7. 锁优化实战(代码中如何应用)
7.1 减少锁持有时间
java
// 反例
synchronized(lock) {
readFile();
processData();
writeDb();
}
// 正例:只锁必要部分
readFile();
synchronized(lock) { processData(); }
writeDb();
7.2 锁粗化
JVM 会自动将连续的对同一锁的加锁解锁合并,例如循环内的 synchronized 可能被优化到循环外。
7.3 锁分离(读写锁,put/take 分离)
LinkedBlockingQueue 内部 put 和 take 使用两个不同的 ReentrantLock,减少竞争。
7.4 无锁化:LongAdder 优于 AtomicLong
java
LongAdder adder = new LongAdder();
// 多个线程同时 add,内部按 Cell 分散,最后 sum 汇总,高并发下性能远高于 AtomicLong 的 CAS 自旋
adder.add(1);
7.5 ThreadLocal 避免共享
java
private static ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
每个线程一份,不加锁,但用后记得 remove() 避免内存泄漏。
8. 死锁案例与排查
8.1 经典死锁代码
java
Object A = new Object(), B = new Object();
new Thread(() -> {
synchronized(A) {
sleep(50);
synchronized(B) { System.out.println("AB"); }
}
}).start();
new Thread(() -> {
synchronized(B) {
sleep(50);
synchronized(A) { System.out.println("BA"); }
}
}).start();
8.2 排查命令
shell
jps # 找到 pid
jstack pid | grep -A 10 "deadlock"
输出会直接告诉你 Found 1 deadlock。
8.3 预防
- 固定加锁顺序。
- 使用
tryLock带超时。 - 死锁检测线程定时打印堆栈。
9. 面试"一口清"串联话术(终极武器)
"Java 的锁我沿两条线掌握:一是 JVM 自动挡
synchronized,从偏向锁到重量级锁逐步膨胀,可重入、自动释放但功能单一。二是基于 AQS 的手动挡ReentrantLock,核心是volatile state和 CLH 等待队列,实现了可中断、可超时、公平锁和Condition分组唤醒。读多写少场景我用ReentrantReadWriteLock或StampedLock的乐观读,注意后者不可重入。JUC 工具我常用
CountDownLatch等任务完成、CyclicBarrier互相等待、Semaphore限流。锁的维度上,我清楚乐观锁(CAS/版本号)和悲观锁的选择,公平和非公平的取舍,以及可重入对避免死锁的意义。调优方面,我会缩小锁粒度、用
LongAdder无锁化、读写锁分离,线上用jstack排查死锁。分布式锁则用 Redisson 的tryLock加看门狗,保证幂等和防悬挂。"
这样一套既有全貌又有细节,每个知识点都能马上给出代码案例,面试官不可能不认可你的深度。