1.2 java面试:java中的锁

要彻底拿下 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(); }
  • 核心基石 AQSAbstractQueuedSynchronizer,用 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 悲观锁

  • 悲观锁 :认为每次操作都会冲突,先加锁再操作。
    synchronizedReentrantLock、数据库 select ... for update
  • 乐观锁 :认为不会冲突,直接操作,提交时检查。
    CAS(AtomicInteger)、版本号(数据库 update ... where version=?)、StampedLock 的乐观读。

2. 公平锁 vs 非公平锁

  • 公平锁 :严格按等待队列顺序,new ReentrantLock(true),吞吐量低,减少饥饿。
  • 非公平锁:允许插队(先 CAS 抢一次),默认,吞吐量高。
  • synchronized 只有非公平。

3. 可重入锁 vs 不可重入锁

  • 可重入 :同一线程再次获取同一把锁,state 加 1,不会死锁。
    synchronizedReentrantLockReentrantReadWriteLock 都是。
  • 不可重入StampedLock,重复获取会死锁,必须注意。

4. 共享锁 vs 排他锁

  • 排他锁(写锁) :同一时刻只有一个线程持有。synchronizedReentrantLock.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 更进一步提供了乐观读,但不可重入。

其他像 CountDownLatchCyclicBarrierSemaphore 都是基于 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 分组唤醒。读多写少场景我用 ReentrantReadWriteLockStampedLock 的乐观读,注意后者不可重入。

JUC 工具我常用 CountDownLatch 等任务完成、CyclicBarrier 互相等待、Semaphore 限流。锁的维度上,我清楚乐观锁(CAS/版本号)和悲观锁的选择,公平和非公平的取舍,以及可重入对避免死锁的意义。

调优方面,我会缩小锁粒度、用 LongAdder 无锁化、读写锁分离,线上用 jstack 排查死锁。分布式锁则用 Redisson 的 tryLock 加看门狗,保证幂等和防悬挂。"

这样一套既有全貌又有细节,每个知识点都能马上给出代码案例,面试官不可能不认可你的深度。