# 美团《不可不说的 Java “锁”事》读后笔记
美团《不可不说的 Java “锁”事》读后笔记
原文:美团技术团队《不可不说的 Java “锁”事》
原文链接:https://tech.meituan.com/2018/11/15/java-lock.html
目录
- 一、文章整体脉络
- 二、Java 锁分类总览
- 三、乐观锁 VS 悲观锁
- 四、CAS:乐观锁的核心实现
- 五、自旋锁 VS 适应性自旋锁
- 六、synchronized 的底层基础:对象头与 Monitor
- 七、synchronized 锁升级:无锁、偏向锁、轻量级锁、重量级锁
- 八、公平锁 VS 非公平锁
- 九、可重入锁 VS 非可重入锁
- 十、独享锁 VS 共享锁
- 十一、AQS 在锁体系中的位置
- 十二、易混点总结
- 十三、面试回答模板
- 十四、最终理解总结
一、文章整体脉络
这篇文章主要讲的是 Java 中常见锁的分类、底层原理和适用场景。
原文不是只讲 synchronized 或 ReentrantLock,而是从“锁的不同特性”出发,把 Java 主流锁分成几组来理解:
- 线程要不要先锁住资源:乐观锁 / 悲观锁
- 线程拿不到锁要不要阻塞:自旋锁 / 非自旋锁
- synchronized 的锁状态变化:无锁 / 偏向锁 / 轻量级锁 / 重量级锁
- 多线程抢锁是否排队:公平锁 / 非公平锁
- 同一个线程能不能重复拿同一把锁:可重入锁 / 非可重入锁
- 一把锁能不能被多个线程共享:独享锁 / 共享锁
重点:Java 锁的分类不是互斥的,而是从不同角度描述同一把锁的特性。
比如 synchronized 可以同时被描述为:
- 从思想上看:它是悲观锁
- 从线程是否可重复进入看:它是可重入锁
- 从资源是否共享看:它是独享锁
- 从 JVM 优化过程看:它可能经历无锁 → 偏向锁 → 轻量级锁 → 重量级锁
二、Java 锁分类总览
原文给出的 Java 主流锁分类图如下:

2.1 我自己的理解
可以把 Java 锁理解成一句话:
Java 锁就是为了解决“多个线程同时操作共享资源”时的数据安全问题,同时还要尽量兼顾性能。
如果不加锁,线程安全可能出问题。
如果一上来就重锁,性能又可能很差。
所以 Java 里才有这么多不同类型的锁,本质上都是在安全性、性能、公平性之间做取舍。
2.2 学习锁时不要死记名字
不要只背:
synchronized、ReentrantLock、CAS、AQS
更应该按问题理解:
| 问题 | 对应锁概念 |
|---|---|
| 操作共享资源前要不要先锁? | 悲观锁 / 乐观锁 |
| 拿不到锁时是阻塞还是等一会儿? | 阻塞 / 自旋 |
| synchronized 如何优化性能? | 偏向锁 / 轻量级锁 / 重量级锁 |
| 抢锁是否严格排队? | 公平锁 / 非公平锁 |
| 同一个线程能不能重复拿锁? | 可重入锁 / 非可重入锁 |
| 一把锁能不能多人一起持有? | 独享锁 / 共享锁 |
三、乐观锁 VS 悲观锁
3.1 悲观锁
悲观锁的核心思想是:
我认为别人一定会来抢这个资源,所以我操作之前必须先加锁。
Java 中常见的悲观锁:
synchronizedReentrantLock
示例:
public synchronized void testMethod() {
// 操作共享资源
}
private final ReentrantLock lock = new ReentrantLock();
public void modifyPublicResources() {
lock.lock();
try {
// 操作共享资源
} finally {
lock.unlock();
}
}
悲观锁特点:
- 操作共享资源之前先加锁
- 没拿到锁的线程会等待
- 适合写操作多、并发冲突严重的场景
重点:悲观锁不是“性能一定差”,而是它默认认为并发冲突很可能发生,所以先加锁保证安全。
3.2 乐观锁
乐观锁的核心思想是:
我先不加锁,等真正要更新数据时,再检查这期间有没有其他线程修改过。
Java 中常见的乐观锁实现:
- CAS
AtomicIntegerAtomicLongAtomicReference- 数据库版本号机制
示例:
private final AtomicInteger atomicInteger = new AtomicInteger();
public int add() {
return atomicInteger.incrementAndGet();
}
乐观锁特点:
- 不会像
synchronized那样直接阻塞线程 - 更新时检查数据有没有被改过
- 适合读多写少、冲突不激烈的场景
重点:乐观锁不是“不保证线程安全”,而是“不通过传统互斥锁阻塞线程”,它通常通过 CAS 保证更新安全。
3.3 图示理解

3.4 对比总结
| 对比项 | 悲观锁 | 乐观锁 |
|---|---|---|
| 核心思想 | 认为一定会冲突 | 认为大概率不会冲突 |
| 是否先加锁 | 是 | 否 |
| 线程是否阻塞 | 可能阻塞 | 通常不阻塞 |
| 常见实现 | synchronized、ReentrantLock | CAS、AtomicInteger |
| 适合场景 | 写多、冲突多 | 读多、冲突少 |
| 风险 | 线程上下文切换成本高 | CAS 失败后可能一直重试 |
四、CAS:乐观锁的核心实现
4.1 CAS 是什么?
CAS 全称是:
Compare And Swap
中文叫:
比较并交换
它的核心逻辑是:
我准备把变量从旧值 A 改成新值 B,但是修改之前要先看一下内存中的值是不是还是 A。
如果还是 A,说明没人改过,我就把它更新成 B。
如果不是 A,说明别人已经改过,这次更新失败,可以重新读取后再试。
CAS 涉及三个值:
| 名称 | 含义 |
|---|---|
| V | 内存中的真实值 |
| A | 期望值 |
| B | 要写入的新值 |
伪代码:
if (V == A) {
V = B;
} else {
// 更新失败,重试或返回失败
}
重点:CAS 的“比较 + 更新”必须是一个原子操作。不能比较完之后还没更新,别的线程就插进来了。
4.2 AtomicInteger 为什么线程安全?
AtomicInteger 的自增底层不是普通的 i++,而是 CAS 自旋。
原文中 AtomicInteger 相关源码图:

getAndAddInt() 的核心逻辑类似:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
原文源码图:

我的理解:
- 先读取当前值
v - 尝试用 CAS 把
v改成v + delta - 如果成功,返回旧值
- 如果失败,说明别的线程已经改过
- 重新读取新值,再次 CAS
- 直到成功为止
重点:AtomicInteger 的线程安全不是因为“没有并发问题”,而是因为它用 CAS + 自旋重试保证最终有一个线程能成功更新。
4.3 CAS 的三个典型问题
4.3.1 ABA 问题
ABA 问题指的是:
A -> B -> A
线程 1 一开始看到变量是 A,准备 CAS。
线程 2 把变量从 A 改成 B,又从 B 改回 A。
线程 1 再次检查时发现还是 A,于是认为变量没被改过。
但实际上,变量中间已经被修改过了。
重点:CAS 只判断“当前值是否等于期望值”,但不知道这个值中间有没有变化过。
解决思路:
给变量加版本号。
1A -> 2B -> 3A
Java 中可以使用:
AtomicStampedReference
它不只比较引用值,还比较版本号。
4.3.2 自旋时间长,CPU 开销大
CAS 失败后通常会不断重试。如果竞争非常激烈,线程可能长时间 CAS 失败,一直循环。
重点:CAS 不会阻塞线程,但失败重试会占用 CPU。冲突越激烈,自旋浪费越明显。
4.3.3 只能保证单个变量的原子性
CAS 对单个变量很适合,比如:
AtomicInteger count;
但如果要同时修改多个变量:
int a;
int b;
普通 CAS 很难保证它们整体原子。
解决思路:
把多个变量封装成一个对象,然后用:
AtomicReference
对整个对象引用做 CAS。
五、自旋锁 VS 适应性自旋锁
5.1 什么是自旋锁?
自旋锁的思想是:
线程拿不到锁时,不马上阻塞,而是在原地循环等一会儿。
通俗理解:
一个线程想进入同步代码块,但是锁被其他线程拿着。
如果持锁线程马上就能执行完,那么当前线程直接阻塞就不划算。
所以它可以先“空转”等几次,看看锁会不会很快释放。
流程图:

5.2 为什么要有自旋?
因为线程阻塞和唤醒需要操作系统参与。
如果同步代码块很短,那么:
线程阻塞 + 唤醒的成本 > 同步代码本身执行成本
这种情况下,线程先自旋等一会儿,可能更划算。
重点:自旋锁是用 CPU 空转,换取减少线程阻塞和唤醒的成本。
5.3 自旋锁适合什么场景?
适合:
- 锁持有时间很短
- 竞争不激烈
- 多核 CPU 环境
不适合:
- 锁持有时间很长
- 竞争激烈
- 很多线程同时自旋
因为大量线程一直自旋,会浪费 CPU。
5.4 适应性自旋锁
普通自旋锁的自旋次数可能是固定的。
适应性自旋锁更智能:
JVM 会根据上一次在同一个锁上的自旋结果,判断这次是否自旋,以及自旋多久。
比如:
- 上一次自旋很快成功,这次可以多自旋一会儿
- 上一次自旋很久都失败,这次可能直接阻塞
重点:适应性自旋不是固定次数,而是根据历史成功率动态调整。
六、synchronized 的底层基础:对象头与 Monitor
6.1 synchronized 锁住的是什么?
synchronized 可以修饰实例方法:
public synchronized void method() {
}
也可以修饰代码块:
synchronized (obj) {
// 临界区
}
本质上,它锁的是对象。
- 修饰实例方法:锁当前对象
this - 修饰静态方法:锁当前类的
Class对象 - 修饰代码块:锁括号里的对象
重点:Java 中每个对象都可以作为一把锁。
6.2 Java 对象头
HotSpot 虚拟机中,对象头主要包括:
- Mark Word
- Klass Pointer
Mark Word
Mark Word 会存储对象运行时信息,比如:
- HashCode
- 分代年龄
- 锁标志位
- 偏向线程 ID
- 指向锁记录的指针
- 指向重量级锁 Monitor 的指针
重点:Mark Word 不是固定只存一种东西,它会根据对象当前状态复用自己的存储空间。
例如:
| 对象状态 | Mark Word 可能存储的内容 |
|---|---|
| 无锁 | hashCode、分代年龄、锁标志位 |
| 偏向锁 | 偏向线程 ID、偏向时间戳、锁标志位 |
| 轻量级锁 | 指向线程栈中 Lock Record 的指针 |
| 重量级锁 | 指向 Monitor 的指针 |
Klass Pointer
Klass Pointer 是对象指向类元数据的指针。
简单理解:
JVM 通过 Klass Pointer 知道这个对象属于哪个类。
6.3 Monitor 是什么?
Monitor 可以理解为对象关联的一把内部锁,也常被称为监视器锁。
每个 Java 对象都可以和一个 Monitor 关联。
Monitor 中有一个 Owner 字段,用来记录当前持有锁的线程。
如果一个线程拿到了对象的 Monitor,就可以进入 synchronized 代码块。
如果其他线程也想进入,只能等待。
重点:synchronized 的重量级锁底层依赖 Monitor,而 Monitor 又依赖操作系统 Mutex Lock,所以线程阻塞和唤醒成本较高。
七、synchronized 锁升级:无锁、偏向锁、轻量级锁、重量级锁
这是本文最重要的部分。
synchronized 的锁状态从低到高是:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
原文锁升级图:

重点:锁可以升级,但不能降级。
7.1 为什么 synchronized 要锁升级?
早期 synchronized 性能较差,是因为它一开始就依赖操作系统互斥锁。
线程阻塞、唤醒需要操作系统参与,成本高。
JDK 6 之后,JVM 对 synchronized 做了优化:
- 没有竞争时,尽量减少加锁成本
- 轻微竞争时,尽量不阻塞线程
- 竞争严重时,再升级为重量级锁
所以才有锁升级机制。
7.2 无锁
无锁状态下,对象没有被传统意义上加锁。
多个线程都可以访问同一个资源,但同一时刻只有一个线程能够修改成功。
典型实现:
CAS
理解:
大家都可以尝试修改,但最后只有 CAS 成功的线程能真正改成功,失败的线程继续重试。
7.3 偏向锁
偏向锁适合这种场景:
一段同步代码几乎总是被同一个线程访问,没有其他线程竞争。
这时 JVM 会认为:
既然总是同一个线程进来,就没必要每次都执行完整的加锁流程。
于是 JVM 会在对象头 Mark Word 中记录偏向线程 ID。
下次同一个线程再进入同步块,只需要检查 Mark Word 里记录的是不是当前线程。
重点:偏向锁的目的,是在没有多线程竞争的情况下,减少不必要的 CAS 操作。
偏向锁什么时候撤销?
偏向锁不会主动释放。
只有当其他线程尝试竞争这把锁时,偏向锁才会撤销。
撤销后可能变成:
- 无锁
- 轻量级锁
难点:偏向锁不是为了处理竞争,而是为了优化“几乎没有竞争,且总是同一个线程反复进入同步块”的情况。
7.4 轻量级锁
轻量级锁适合这种场景:
已经出现线程竞争,但竞争不严重,锁很快就会释放。
轻量级锁的核心是:
CAS + 自旋
大致过程:
- 线程进入同步代码块
- JVM 在线程栈帧中创建 Lock Record
- 把对象头 Mark Word 复制到 Lock Record
- 用 CAS 尝试把对象 Mark Word 更新为指向 Lock Record 的指针
- 如果 CAS 成功,当前线程获得锁
- 如果 CAS 失败,说明有竞争
- 线程先通过自旋尝试等待锁释放
重点:轻量级锁不是没有竞争,而是“竞争不激烈时,先别急着阻塞线程”。
7.5 重量级锁
当竞争变严重时,轻量级锁会升级为重量级锁。
例如:
- 自旋很多次仍然拿不到锁
- 一个线程持有锁,一个线程自旋,又来了更多线程竞争
重量级锁状态下:
- Mark Word 中存储指向 Monitor 的指针
- 没有拿到锁的线程会阻塞
- 阻塞和唤醒依赖操作系统
重点:重量级锁开销大,但竞争激烈时它更合适,因为不能让大量线程一直自旋浪费 CPU。
7.6 四种锁状态总结
| 锁状态 | 适合场景 | 核心机制 | 是否阻塞 |
|---|---|---|---|
| 无锁 | 没有加锁,CAS 修改 | CAS | 否 |
| 偏向锁 | 同一个线程反复进入 | Mark Word 记录线程 ID | 否 |
| 轻量级锁 | 有竞争但不激烈 | CAS + 自旋 | 尽量不阻塞 |
| 重量级锁 | 竞争激烈 | Monitor + 操作系统互斥锁 | 是 |
7.7 面试回答模板
synchronized 在 JDK 6 以后做了很多优化,它不是一上来就是重量级锁。它会根据竞争程度进行锁升级,大致过程是无锁、偏向锁、轻量级锁、重量级锁。
如果基本没有竞争,并且总是同一个线程访问同步代码块,会使用偏向锁,在对象头 Mark Word 中记录线程 ID,减少加锁成本。
如果出现轻微竞争,会升级为轻量级锁,通过 CAS 和自旋尝试获取锁,避免线程阻塞。
如果竞争比较激烈,自旋也拿不到锁,就升级为重量级锁,其他线程进入阻塞状态。
这样做的目的就是根据竞争程度选择更合适的锁实现,在安全性和性能之间做平衡。
八、公平锁 VS 非公平锁
8.1 公平锁
公平锁的思想是:
多个线程按照申请锁的顺序排队,先来先得。
优点:
- 等待锁的线程不容易饿死
- 顺序更公平
缺点:
- 吞吐量较低
- 队列中的线程经常需要阻塞和唤醒
- CPU 唤醒阻塞线程有额外开销
原文公平锁图示:

8.2 非公平锁
非公平锁的思想是:
新来的线程可以先尝试抢锁,抢不到再去排队。
也就是说,即使队列中已经有线程在等待,如果锁刚好释放,新来的线程也可能直接抢到。
优点:
- 吞吐量高
- 减少线程阻塞和唤醒成本
缺点:
- 可能导致某些线程等很久
- 极端情况下可能产生线程饥饿
原文非公平锁图示:

8.3 ReentrantLock 默认是公平锁还是非公平锁?
ReentrantLock 默认是非公平锁。
ReentrantLock lock = new ReentrantLock();
如果想创建公平锁:
ReentrantLock lock = new ReentrantLock(true);
原文源码图:

8.4 公平锁和非公平锁源码差异
公平锁获取锁之前会判断队列中是否有前驱节点:
hasQueuedPredecessors()
非公平锁则是先尝试 CAS 抢锁,抢不到再排队。
源码对比图:


重点:公平锁是“先看有没有人排队”;非公平锁是“先抢一下,抢不到再排队”。
九、可重入锁 VS 非可重入锁
9.1 什么是可重入锁?
可重入锁又叫递归锁。
它的含义是:
同一个线程已经获取了一把锁之后,再次进入需要同一把锁的代码时,不会被自己阻塞。
示例:
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
线程进入 doSomething() 时已经拿到了当前对象的锁。
然后调用 doOthers(),这个方法也需要当前对象的锁。
因为 synchronized 是可重入锁,所以同一个线程可以继续进入。
重点:如果 synchronized 不可重入,那么线程会自己等自己,造成死锁。
9.2 Java 中哪些锁是可重入锁?
常见可重入锁:
synchronizedReentrantLock
9.3 可重入锁图示

非可重入锁图示:

9.4 可重入锁底层怎么实现?
以 ReentrantLock 为例,它底层基于 AQS。
AQS 中有一个 state 变量,用来记录锁状态。
可以这样理解:
state = 0:没有线程持有锁
state = 1:某个线程持有锁一次
state = 2:同一个线程重入了一次
state = 3:同一个线程重入了两次
加锁过程:
- 如果
state == 0,说明锁没人持有,当前线程可以获取锁 - 如果
state != 0,判断持有锁的是不是当前线程 - 如果是当前线程,说明发生重入,
state + 1 - 如果不是当前线程,就进入等待
释放锁过程:
- 当前线程每释放一次锁,
state - 1 - 直到
state == 0 - 锁才真正释放
源码对比图:

重点:可重入锁不是“重复加锁不记录”,而是通过计数器记录重入次数。加锁几次,就要释放几次。
十、独享锁 VS 共享锁
10.1 独享锁
独享锁也叫排他锁。
含义是:
一把锁一次只能被一个线程持有。
常见独享锁:
synchronizedReentrantLockReentrantReadWriteLock的写锁
独享锁适合:
- 修改数据
- 写操作
- 不允许多个线程同时进入的场景
10.2 共享锁
共享锁含义是:
一把锁可以被多个线程同时持有。
常见共享锁:
ReentrantReadWriteLock的读锁
共享锁适合:
- 读操作
- 多个线程可以同时读取
- 但读的时候不能有线程写
10.3 ReentrantReadWriteLock:读写锁
ReentrantReadWriteLock 里面有两把锁:
ReadLockWriteLock
原文源码结构图:

读锁是共享锁。
写锁是独享锁。
规则:
| 场景 | 是否互斥 | 说明 |
|---|---|---|
| 读读 | 不互斥 | 多个线程可以同时读 |
| 读写 | 互斥 | 读的时候不能写 |
| 写读 | 互斥 | 写的时候不能读 |
| 写写 | 互斥 | 同一时刻只能一个线程写 |
重点:读写锁适合读多写少场景。多个读线程可以同时读,提高并发能力;只要涉及写,就必须互斥。
10.4 读写锁中的 state 拆分
AQS 的 state 是一个 int 类型,32 位。
在 ReentrantReadWriteLock 中,它把 state 分成两部分:
- 高 16 位:读锁数量
- 低 16 位:写锁数量
图示:

理解:
高 16 位记录读锁状态
低 16 位记录写锁状态
难点:读写锁虽然有两把锁,但底层仍然依赖同一个 AQS 的 state,只是把一个 int 按位拆成了读状态和写状态。
十一、AQS 在锁体系中的位置
AQS 全称:
AbstractQueuedSynchronizer
它不是一把具体的锁,而是 JUC 里很多锁和同步工具的底层框架。
常见基于 AQS 的工具:
ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch
AQS 主要做两件事:
- 用
state表示同步状态 - 用同步队列管理等待线程
简单理解:
AQS 就像一个半成品锁框架,具体是独享锁、共享锁、公平锁、非公平锁,由子类通过不同方法实现。
重点:面试中不要说 AQS 是某一把锁。AQS 是实现锁和同步器的基础框架。
十二、易混点总结
12.1 CAS 和自旋是不是一回事?
不是。
| 概念 | 含义 |
|---|---|
| CAS | 一种原子操作,比较并交换 |
| 自旋 | 一种等待方式,失败后循环重试 |
它们经常一起出现,比如 AtomicInteger:
CAS 失败 -> 自旋重试 -> 再 CAS
重点:CAS 是操作,自旋是等待方式。
12.2 乐观锁一定比悲观锁好吗?
不一定。
乐观锁适合:
- 读多写少
- 冲突少
- CAS 成功率高
悲观锁适合:
- 写多
- 冲突多
- 临界区复杂
重点:没有绝对更好的锁,只有更适合当前场景的锁。
12.3 synchronized 是不是重量级锁?
不能简单这么说。
早期 synchronized 确实比较重。
但是 JDK 6 之后,synchronized 有锁升级机制:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
所以现在说 synchronized,要知道它会根据竞争情况优化。
12.4 偏向锁和轻量级锁的区别
| 对比项 | 偏向锁 | 轻量级锁 |
|---|---|---|
| 适用场景 | 几乎没有竞争 | 有竞争但不激烈 |
| 核心目的 | 减少同一线程重复加锁成本 | 避免线程阻塞 |
| 核心机制 | Mark Word 记录线程 ID | CAS + 自旋 |
| 竞争情况 | 基本无竞争 | 轻微竞争 |
一句话区分:
偏向锁解决“同一个线程反复进入”的成本问题;轻量级锁解决“轻微竞争时不想阻塞线程”的问题。
12.5 公平锁一定好吗?
不一定。
公平锁优点:
- 排队有序
- 不容易饥饿
公平锁缺点:
- 吞吐量低
- 线程唤醒成本高
非公平锁虽然可能插队,但吞吐量通常更高。
所以 ReentrantLock 默认是非公平锁。
12.6 可重入锁是不是不用释放多次?
不是。
示例:
lock.lock(); // state = 1
lock.lock(); // state = 2
lock.unlock(); // state = 1
lock.unlock(); // state = 0,真正释放
重点:可重入锁加锁几次,就要释放几次。否则锁不会真正释放。
十三、面试回答模板
13.1 什么是乐观锁和悲观锁?
悲观锁认为并发冲突很可能发生,所以操作共享资源之前会先加锁,比如
synchronized和ReentrantLock。
乐观锁认为冲突不一定发生,所以不会先加锁,而是在更新数据时判断数据有没有被其他线程修改过,典型实现是 CAS,比如AtomicInteger。
悲观锁适合写多、冲突多的场景;乐观锁适合读多、冲突少的场景。
13.2 CAS 是什么?有什么问题?
CAS 是 Compare And Swap,比较并交换。它会比较内存中的值和期望值是否一致,如果一致,就把内存值更新成新值;如果不一致,说明数据被其他线程改过,本次更新失败,可以重试。
CAS 的优点是不需要阻塞线程,性能比较高。
但它也有问题:第一是 ABA 问题;第二是高竞争下会一直自旋,浪费 CPU;第三是普通 CAS 只能保证单个变量的原子性。
13.3 synchronized 的锁升级过程是什么?
synchronized 在 JDK 6 以后做了很多优化,不是一上来就是重量级锁。它的锁状态大致是无锁、偏向锁、轻量级锁、重量级锁。
如果没有竞争,或者总是同一个线程进入同步代码块,会使用偏向锁,减少加锁成本。
如果出现轻微竞争,会升级为轻量级锁,通过 CAS 和自旋尝试获取锁,避免线程阻塞。
如果竞争比较激烈,自旋也拿不到锁,就升级成重量级锁,其他线程进入阻塞状态。
锁升级的目的就是根据竞争程度选择更合适的锁实现。
13.4 什么是自旋锁?
自旋锁是指线程拿不到锁时,不马上阻塞,而是循环等待一小段时间。
如果锁很快释放,线程就可以直接拿到锁,避免线程阻塞和唤醒的成本。
但如果锁长时间不释放,自旋线程会一直占用 CPU,所以自旋适合锁持有时间很短、竞争不激烈的场景。
13.5 公平锁和非公平锁有什么区别?
公平锁是按照线程申请锁的顺序排队,先来先得。优点是线程不容易饿死,缺点是吞吐量低。
非公平锁是线程先尝试抢锁,抢不到再进入队列。它可能导致后来的线程先拿到锁,但吞吐量更高。
ReentrantLock默认是非公平锁,因为性能更好;如果要公平锁,可以使用new ReentrantLock(true)。
13.6 什么是可重入锁?
可重入锁指的是同一个线程已经获取了一把锁之后,再次进入需要同一把锁的代码时,不会被自己阻塞。
synchronized和ReentrantLock都是可重入锁。
可重入锁底层通常会记录重入次数,比如ReentrantLock通过 AQS 的state记录。每加锁一次state + 1,每释放一次state - 1,直到state = 0才真正释放锁。
13.7 独享锁和共享锁有什么区别?
独享锁是一次只能被一个线程持有,比如
synchronized、ReentrantLock、写锁。
共享锁可以被多个线程同时持有,比如ReentrantReadWriteLock的读锁。
读写锁中,读读可以共享,读写、写读、写写互斥,所以它适合读多写少的场景。
十四、最终理解总结
这篇文章表面上是在讲很多种锁,但核心其实是一个问题:
多线程同时访问共享资源时,Java 如何在“线程安全”和“性能”之间做平衡?
我的最终理解:
- 悲观锁适合冲突多的场景,先加锁再操作。
- 乐观锁适合冲突少的场景,更新时再判断有没有被改过。
- CAS是乐观锁的重要实现,但有 ABA、自旋开销和单变量限制。
- 自旋锁是不阻塞线程,而是让线程短暂空转等待锁释放。
- synchronized不是简单的重量级锁,它有锁升级机制。
- 偏向锁优化同一个线程反复进入同步块的场景。
- 轻量级锁优化轻微竞争场景,用 CAS + 自旋避免阻塞。
- 重量级锁适合竞争激烈场景,让线程阻塞,避免 CPU 空转。
- 公平锁更讲究顺序,但吞吐量较低。
- 非公平锁允许插队,吞吐量更高,
ReentrantLock默认就是非公平锁。 - 可重入锁允许同一线程重复获取同一把锁,但释放次数必须对应。
- 读写锁适合读多写少,读读共享,涉及写就互斥。
- AQS不是具体锁,而是很多 JUC 锁和同步器的底层框架。
最终一句话:
Java 锁的本质,是根据并发竞争程度和业务场景,在安全性、性能、公平性之间做取舍。
十五、自测题
基础题
- Java 为什么需要锁?
- 乐观锁和悲观锁的区别是什么?
- CAS 的三个操作数是什么?
- AtomicInteger 为什么线程安全?
- CAS 有哪些缺点?
进阶题
- synchronized 的锁升级过程是什么?
- 偏向锁和轻量级锁有什么区别?
- 自旋锁为什么不能一直自旋?
- ReentrantLock 默认公平锁还是非公平锁?为什么?
- 什么是可重入锁?底层如何记录重入次数?
- 读写锁为什么适合读多写少?
- AQS 中 state 的作用是什么?
面试场景题
- 如果一个接口高并发下需要统计访问次数,你会用 synchronized 还是 AtomicInteger?为什么?
- 如果一个共享资源写操作非常多,还适合用 CAS 吗?为什么?
- 如果多个线程只是读缓存数据,很少写,你会考虑什么锁?
- 如果 synchronized 竞争激烈,会发生什么?
- 如果 ReentrantLock 加锁两次,只释放一次,会发生什么?
十七、我最需要重点背的内容
1. CAS 是比较并交换,涉及内存值 V、期望值 A、新值 B。
2. CAS 的三个问题:ABA、自旋开销大、只能保证单变量原子性。
3. synchronized 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
4. 偏向锁解决同一个线程重复进入同步块的成本问题。
5. 轻量级锁通过 CAS + 自旋避免线程阻塞。
6. 重量级锁会让等待线程阻塞,依赖 Monitor 和操作系统互斥锁。
7. ReentrantLock 默认非公平锁,因为吞吐量更高。
8. 可重入锁通过 state 记录重入次数,加锁几次就要释放几次。
9. 读写锁中读锁共享,写锁独占,适合读多写少。
10. AQS 是很多 JUC 锁和同步工具的底层框架,不是一把具体的锁。
浙公网安备 33010602011771号