说一下Java中的锁

Java 中的“锁”是一个非常大的话题,可以从不同维度进行分类。下面我按面试常问的脉络帮你梳理一遍,从基础概念到进阶原理。


一、按锁的性质/行为分类

1. 乐观锁 vs 悲观锁

维度 悲观锁 乐观锁
核心思想 认为并发冲突一定会发生,先加锁再操作 认为冲突很少发生,操作时不加锁,提交时再校验
实现方式 synchronized、ReentrantLock、数据库 select ... for update CAS(Compare And Swap)、版本号机制
适用场景 写多读少、冲突激烈 读多写少、冲突较少
缺点 加锁释放锁开销大,可能死锁 失败重试开销大,ABA 问题

ABA 问题:用 CAS 时,值从 A 改成 B 又改回 A,CAS 检查不出变化。可用 AtomicStampedReference 加版本号解决。


2. 公平锁 vs 非公平锁

  • 公平锁:线程按申请锁的顺序获取锁,先来先得。new ReentrantLock(true)。
  • 非公平锁:允许插队,新来的线程可能直接抢到锁。synchronized 和 new ReentrantLock() 默认都是非公平。

为什么默认非公平? 非公平锁吞吐量更高。因为唤醒一个排队线程需要上下文切换,而新来的线程可能刚好在 CPU 上,直接拿到锁能减少切换开销。


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

  • 可重入锁:同一线程可以多次获取同一把锁,不会死锁。synchronized 和 ReentrantLock 都是可重入的。
  • 不可重入锁:同一线程再次获取锁会被阻塞。早期的一些自研锁可能是这样。

可重入的实现:锁内部维护持有线程和重入次数,每次获取 +1,释放 -1,减到 0 才真正释放。


4. 独占锁 vs 共享锁

  • 独占锁(排他锁):同一时刻只允许一个线程持有。ReentrantLock、synchronized。
  • 共享锁:同一时刻允许多个线程持有。ReentrantReadWriteLock 的读锁、Semaphore、CountDownLatch。

ReentrantReadWriteLock 就是读写分离:读读不互斥,读写互斥,写写互斥。


二、按 JVM 实现层面分类

5. 偏向锁 → 轻量级锁 → 重量级锁(锁升级)

这是 synchronized 在 HotSpot 中的优化路径,锁状态记录在对象头的 Mark Word 中:

锁状态 适用场景 原理
无锁 无并发 —
偏向锁 只有一个线程访问 Mark Word 记录线程 ID,同一线程进入不需 CAS
轻量级锁 少量线程交替执行 CAS 自旋尝试获取锁,不阻塞
重量级锁 多线程激烈竞争 依赖操作系统互斥量(mutex),线程阻塞挂起

升级过程不可逆(偏向锁可批量撤销,但整体是单向升级)。

JDK 15 之后偏向锁默认禁用,JDK 18 彻底移除。因为现代应用并发高,偏向锁收益有限且维护成本大。


6. 自旋锁 vs 自适应自旋锁

  • 自旋锁:线程获取不到锁时不立即阻塞,而是循环重试(自旋),避免上下文切换。
  • 自适应自旋:JVM 根据上次自旋成功率动态调整自旋次数。成功过就多转几次,总失败就少转或不转。

自旋适合锁持有时间短的场景,否则空转浪费 CPU。


三、JDK 并发包(JUC)中的具体锁实现

7. AQS(AbstractQueuedSynchronizer)

JUC 锁的基石,内部维护:

  • 一个 volatile int state 表示同步状态
  • 一个 CLH 双向队列存放等待线程
  • ConditionObject 支持条件等待

ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS 实现。


8. ReentrantLock

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock(); // 必须手动释放
}

特点:

  • 可重入、可中断(lockInterruptibly)、可超时(tryLock(timeout))
  • 支持公平/非公平
  • 支持多个条件变量 Condition

与 synchronized 对比:

维度 synchronized ReentrantLock
实现层面 JVM 内置 JDK API(AQS)
释放方式 自动释放 手动 finally 释放
可中断 否 是
超时获取 否 是
公平锁 否 可选
条件变量 单个 wait/notify 多个 Condition

9. ReentrantReadWriteLock

读写分离,提升读多写少场景性能:

  • 读锁共享,写锁独占
  • 支持锁降级(写锁 → 读锁),不支持锁升级
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
rwLock.writeLock().lock();

10. StampedLock(JDK 8+)

比 ReentrantReadWriteLock 性能更好的读写锁,支持三种模式:

  • 写锁:独占
  • 悲观读锁:共享
  • 乐观读:不加锁,读取后校验 stamp 是否变化
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// 读操作
if (!lock.validate(stamp)) {
    stamp = lock.readLock(); // 升级为悲观读
    try { /* 重新读 */ } finally { lock.unlockRead(stamp); }
}

注意:StampedLock 不可重入,且不支持 Condition。


11. 其他同步工具(广义的“锁”)

工具 作用
Semaphore 信号量,控制同时访问的线程数
CountDownLatch 倒计时门闩,等待 N 个任务完成
CyclicBarrier 循环栅栏,一组线程互相等待到齐
Phaser 更灵活的多阶段同步

四、面试高频追问点

  1. synchronized 和 Lock 的区别? → 见上表
  2. synchronized 锁升级过程? → 偏向 → 轻量 → 重量
  3. AQS 原理? → state + CLH 队列 + CAS
  4. CAS 的缺点? → ABA 问题、自旋开销、只能保证一个变量原子性
  5. 死锁的四个必要条件? → 互斥、请求保持、不可剥夺、循环等待
  6. 如何避免死锁? → 按序加锁、超时释放、一次性申请所有资源
  7. ReentrantLock 公平锁怎么实现? → AQS 中 hasQueuedPredecessors() 判断是否有前驱节点

五、一句话总结

Java 的锁可以从 乐观/悲观、公平/非公平、可重入/不可重入、独占/共享 等维度理解;JVM 层面 synchronized 有锁升级优化;JUC 层面以 AQS 为核心,衍生出 ReentrantLock、ReentrantReadWriteLock、StampedLock 等实现,配合 Semaphore、CountDownLatch 等工具应对不同并发场景。

如果你需要我针对某一块(比如 AQS 源码、锁升级细节、死锁排查)展开讲,可以告诉我。

posted @ 2026-10-08 17:00  ~码铃薯~  阅读(12)  评论(0)    收藏  举报