Java 中常见的锁有哪些?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 体系化认知:面试官不仅仅是想知道你用过哪些锁,更是想知道你能不能从不同维度(锁的设计思想、实现方式、竞争策略等)把锁分类讲清楚。零散背诵和体系化理解,在面试官眼里一眼就能看出来。

  2. 原理深度:考察你是否理解 synchronized 的锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),ReentrantLock 的 AQS 实现原理,以及 CAS 乐观锁的底层支持。

  3. 实战选型能力:不同的业务场景该用什么锁?为什么?能不能讲清楚 synchronizedReentrantLock 的选型依据。

核心答案

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;
对比悲观锁乐观锁
思想先锁再操作,防患于未然先操作再检测,冲突了再重试
实现synchronizedReentrantLockCAS、版本号
适用写多读少、竞争激烈读多写少、冲突概率低
性能加锁有开销,但保证一致性无锁开销,冲突时重试有成本

二、公平锁 vs 非公平锁

获取锁的顺序 角度分。

  • 公平锁:多个线程按申请锁的顺序排队,先到先得。不会出现 "饥饿" 现象,但吞吐量低。
  • 非公平锁:新来的线程直接尝试插队获取锁,失败了再去排队。吞吐量高,但可能导致排队线程长时间获取不到锁。
// ReentrantLock 支持选择公平/非公平
ReentrantLock fairLock = new ReentrantLock(true);     // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false);   // 非公平锁(默认)

// synchronized 是非公平锁(无法设置为公平锁)

ReentrantLock 默认用非公平锁。为什么?因为公平锁每次都要检查队列里有没有排队的线程,涉及线程切换开销;而非公平锁直接尝试 CAS 抢锁,抢到了就省去了一次线程切换,性能更好。生产环境绝大部分场景用非公平锁就够了。

三、独享锁(排他锁)vs 共享锁

能否被多个线程同时持有 角度分。

  • 独享锁(也叫排他锁、互斥锁):同一时刻只能被一个线程持有。synchronizedReentrantLock 都是独享锁。
  • 共享锁:可以被多个线程同时持有。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 里 synchronizedReentrantLock 都是可重入的,所以日常开发中不用担心这个问题。

五、synchronized 的锁升级(JDK 6+)

这块是面试的高频考点。JDK 6 之后,synchronized 引入了 "锁升级" 机制,根据竞争情况从低级锁逐步升级到高级锁:

上图展示了 synchronized 锁升级的完整过程。整体分四个阶段:

  • 无锁:对象刚创建,还没有任何线程来访问同步代码。

  • 偏向锁:第一个线程访问时,JVM 把线程 ID 写入对象头的 Mark Word 中。之后这个线程再次进入同步块,发现 Mark Word 里的线程 ID 是自己,直接进入,连 CAS 操作都不需要。本质上是 "偏心"——锁偏向第一个获取它的线程。

  • 轻量级锁:当第二个线程来竞争偏向锁时,偏向锁撤销,升级为轻量级锁。此时竞争的线程通过 CAS 自旋 尝试获取锁。自旋就是不停地循环尝试,不放弃 CPU 执行时间。适合锁持有时间短的场景——线程稍微等几轮就能拿到锁。

  • 重量级锁:如果自旋了一定次数还没拿到锁,说明竞争激烈,升级为重量级锁。此时拿不到锁的线程会被 挂起(阻塞),交由操作系统调度。涉及用户态到内核态的切换,开销比较大。

关键点:锁升级是 单向 的,只能从低到高升级,不能降级(不过有特例,比如在全局安全点 STW 期间可以降级)。这个设计的核心思想是 延迟重量级锁的使用——大部分情况下锁竞争并不激烈,用轻量级的方案就够了。

六、synchronized vs ReentrantLock

这是面试中被问频率最高的一组对比,放个表格一目了然:

对比维度synchronizedReentrantLock
实现层面JVM 关键字,底层用 MonitorJDK API,基于 AQS
加锁方式自动加锁释放手动 lock() / unlock()
是否可中断不可中断可中断(lockInterruptibly()
公平性只支持非公平支持公平和非公平(构造参数)
条件变量只有一个等待队列(wait/notify支持多个条件队列(Condition
锁升级支持偏向锁 → 轻量级锁 → 重量级锁不支持,直接 CAS + AQS
适用场景简单同步、代码块保护需要高级功能(超时、中断、公平)

怎么选?简单的同步场景用 synchronized,代码更简洁,JVM 层面持续优化(锁升级、锁消除、锁粗化)。需要超时获取、可中断、公平锁、多条件队列这些高级功能时用 ReentrantLock

七、自旋锁

自旋锁的核心思路就是:拿不到锁?不死等,先 "原地转圈" 试几次。

// 自旋锁的简单实现
class SpinLock {
    private AtomicReference<Thread> owner = new 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(对每个桶的头节点加锁),并发度从 "段数" 提升到 "桶数",粒度更细。

面试高频追问

  1. synchronizedReentrantLock 怎么选?

    • 简单同步用 synchronized(代码简洁、JVM 持续优化),需要高级功能(可中断、超时、公平、多条件)用 ReentrantLock
  2. 什么是 CAS?有什么问题?

    • Compare And Swap,乐观锁的底层实现。问题有三个:ABA 问题(用 AtomicStampedReference 解决)、自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量的原子性(用 AtomicReference 包装多个变量)。
  3. 什么是锁消除和锁粗化?

    • 锁消除是 JIT 优化——检测到不可能被共享的对象上的 synchronized 会自动消除。锁粗化是 JIT 把多次连续的加锁解锁合并为一次,减少开销。
  4. synchronized 锁升级能降级吗?

    • 正常情况下不能,只升不降。但在 STW(全局安全点)期间,JVM 允许做降级优化。

常见面试变体

  • "说说 synchronizedReentrantLock 的区别?"
  • "什么是 CAS?ABA 问题怎么解决?"
  • "synchronized 的锁升级过程是怎样的?"
  • "什么是公平锁?synchronized 是公平锁吗?"

记忆口诀

锁分类六大维度:思想(悲观/乐观)、策略(公平/非公平)、持有(独享/共享)、重入(可/不可)、状态(偏向→轻量→重量)、实现(关键字/API)。

总结

Java 锁的分类不是孤立的,同一个锁从不同维度看会有不同 "身份"。比如 synchronized 是悲观锁、非公平锁、独享锁、可重入锁,还支持锁升级。面试时先给出分类体系的全景图,再挑 2-3 个重点维度深入展开(推荐悲观锁/乐观锁和锁升级),面试官就知道你是有体系地掌握了。