Java 中常见的锁有哪些?

Java 中常见的锁有哪些?

面试考察点

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

原理深度:考察你是否理解 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 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(对每个桶的头节点加锁),并发度从 "段数" 提升到 "桶数",粒度更细。

面试高频追问

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 个重点维度深入展开(推荐悲观锁/乐观锁和锁升级),面试官就知道你是有体系地掌握了。

相关推荐

safety更多的中文(繁體)翻譯
beat365网站假的吗

safety更多的中文(繁體)翻譯

🎯 01-18 💯 824
如何购买MyCard点卡
beat365网站假的吗

如何购买MyCard点卡

🎯 07-08 💯 526