目录
[4. 重量级锁(高竞争场景)](#4. 重量级锁(高竞争场景))
synchronized的底层实现:
synchronized核心是通过 对象头(Mark Word) 和 管程(Monitor) 控制线程互斥。再在JDK 1.6 引入的 锁升级 机制大幅优化了其性能。
synchronized
的锁机制并非一开始就是重量级锁,而是根据并发竞争强度 动态升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),以平衡性能和安全性。
锁升级
1.无锁状态(初始状态)
指对象刚被创建,未被任何线程加锁,可以被任何线程使用~
2.偏向锁(单线程优化)
- 触发条件 :当第一个线程(线程 A)尝试获取锁时,JVM 会通过 CAS 操作将
Mark Word
修改为偏向锁状态,并记录线程 A 的 ID(此时无需真正 "加锁",只是标记偏向的线程)。
- 升级过程 :
- 当第一个线程(线程 A)尝试获取锁时,JVM 通过 CAS 操作将
Mark Word
修改为偏向锁状态和 Mark Word 中的 "偏向标志" 设为1
,并写入线程 A 的 ID。 - 后续线程 A 再次获取锁时,只需对比 Mark Word 中的线程 ID 是否为自己,无需 CAS 操作(几乎无开销)。
- 当第一个线程(线程 A)尝试获取锁时,JVM 通过 CAS 操作将
- Mark Word 变化 :存储偏向线程 ID,状态标记仍为
01
(但偏向标志为1
,区分于无锁)。
- 优化: 优化了单线程重复获取锁 的场景(现实中多数锁在一段时间内仅被一个线程使用)。
3.轻量级锁(低竞争优化)
- 设计目的 :应对 少量线程短时间竞争 的场景,避免直接进入重量级锁(减少内核态切换开销)。
- 触发条件 :当有第二个线程(线程 B)尝试获取锁时,发现
Mark Word
中记录的是线程 A 的 ID(偏向锁状态),此时会触发偏向锁的撤销:- 若线程 A 已退出同步块(锁已释放),则将
Mark Word
重置为无锁状态,线程 B 通过 CAS 将其改为轻量级锁状态(记录线程 B 的锁记录指针)。 - 若线程 A 仍持有锁,则偏向锁升级为轻量级锁,线程 A 和线程 B 通过自旋(空转等待)尝试获取锁。
- 若线程 A 已退出同步块(锁已释放),则将
- 升级过程 :
- 线程在自己的 栈帧 中创建 锁记录(Lock Record),存储当前 Mark Word 的副本(Displaced Mark Word)。
- 通过 CAS 操作 将对象的 Mark Word 改为 指向当前线程锁记录的指针 (状态标记改为
00
)。 - 若 CAS 成功,线程获取锁;若失败(说明有竞争),线程进入 自旋(Spin) 重试(循环尝试 CAS,不放弃 CPU)。
- 自旋意义:短时间竞争下,自旋可能在锁释放前成功获取,避免阻塞(用户态操作,开销小于内核态)。
- 触发升级:若自旋失败(如自旋次数超过阈值,或竞争激烈),轻量级锁升级为重量级锁。
4. 重量级锁(高竞争场景)
- 设计目的 :应对 多线程激烈竞争 或 长时间持有锁 的场景,依赖操作系统保证互斥。
- 实现依赖 :操作系统的 互斥量(Mutex) 和 JVM 的 管程(Monitor) 。
- Monitor 是一个 C++ 对象,包含
owner
(持有锁的线程)、EntryList
(等待锁的线程队列)、WaitSet
(调用wait()
等待的线程队列)。
- Monitor 是一个 C++ 对象,包含
- 触发条件:当自旋结束后,线程仍未获取到锁(例如线程持有锁的时间很长,或竞争线程过多),轻量级锁会膨胀为重量级锁:
- 升级过程 :
- 锁升级为重量级后,Mark Word 改为 指向 Monitor 的指针 (状态标记改为
10
)。 - 未获取锁的线程进入
EntryList
并 阻塞(放弃 CPU,进入内核态),等待持有锁的线程释放后被唤醒。
- 锁升级为重量级后,Mark Word 改为 指向 Monitor 的指针 (状态标记改为
- 特点:开销大(用户态 ↔ 内核态切换),但能保证高竞争场景下的线程安全。
- 用户态→内核态切换的高开销 :源于上下文保存、页表切换、安全检查等硬件和软件层面的复杂操作,本质是 "权限隔离" 的代价。
-
上下文切换的硬件开销
切换时,CPU 必须保存当前用户态的上下文信息(如程序计数器、栈指针、通用寄存器值等)到内存(通常是内核栈),再加载内核态的上下文。这个过程涉及多次内存读写(内存速度比 CPU 寄存器慢 100 倍以上),且需要严格保证操作的原子性(避免数据错乱),硬件层面的指令执行成本很高。
-
锁消除:
编译器在编译时会分析代码,如果发现某些锁不可能被多线程访问,会自动消除:
java
// 原始代码
public String concatString(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // 局部变量
sb.append(s1);
sb.append(s2);
return sb.toString();
}
// StringBuffer的append方法是synchronized的
// 但由于sb是局部变量,编译器会消除这些锁
锁粗化:
若多个连续的同步块使用同一把锁,JVM 会将它们合并为一个大的同步块,减少加锁 / 解锁的次数。
java
// 原始代码:频繁加锁解锁
for (int i = 0; i < 1000; i++) {
synchronized(obj) {
// 操作
}
}
// 优化后:扩大锁的范围
synchronized(obj) {
for (int i = 0; i < 1000; i++) {
// 操作
}
}
ReentrantLock的底层实现
ReentrantLock
的底层完全依赖 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现。AQS 是 Java 并发工具的基础框架,通过两个核心部分管理同步逻辑:
- 同步状态(
state
):一个volatile int
变量,用于记录锁的持有状态(state=0
表示未锁定,state>0
表示被持有,数值等于重入次数),还有一个exclusiveOwnerThread (持锁线程 )。 - 双向阻塞队列:当线程获取锁失败时,会被包装成
Node
节点加入队列,按 FIFO 顺序等待被唤醒(类似 "等待队列")。
非公平锁获取锁(lock)

- 首次抢锁 :线程直接通过 CAS 尝试将
state
从 0 改为 1(compareAndSetState(0, 1)
)。若成功,标记当前线程为锁持有者(setExclusiveOwnerThread
)。 - 抢锁失败 :若 CAS 失败(锁已被持有),调用 AQS 的
acquire(1)
方法,进入后续流程:tryAcquire
重试 :检查锁是否被当前线程持有(可重入性),若是则递增state
(重入次数 + 1);若不是,则抢锁失败。- 入队阻塞 :抢锁失败的线程被包装成
Node
节点,通过addWaiter
加入队列尾部,再通过acquireQueued
自旋等待(或阻塞),直到前驱节点释放锁并唤醒自己。
公平锁与非公平锁的区别

公平锁会检查队列中是否有等待的线程,非公平锁直接抢占,这是性能差异的根本原因"
锁释放

- 释放逻辑 :调用
unlock()
时,实际调用 AQS 的release(1)
方法,核心是tryRelease
方法:- 递减
state
(重入次数 - 1),若当前线程不是持有者则抛异常(保证安全性)。 - 当
state
减为 0 时,清除持有者标记,返回true
表示完全释放锁。
- 递减
- 唤醒线程 :完全释放锁后,通过
unparkSuccessor
唤醒队列中等待的后继线程,使其重新竞争锁。
"释放锁时会检查重入次数,只有state减到0才真正释放,然后唤醒队列中的下一个线程"
当然ReentrantLock
的核心价值在于 灵活性和可控性,其功能覆盖了基础互斥、可重入、公平性选择、中断响应、超时控制、多条件等待等场景,适合复杂并发逻辑的实现。但需注意手动释放锁(通常在 finally
中),避免死锁风险。
使用场景:适用场景的差异
-
优先用
synchronized
的场景:- 简单同步需求(如普通方法 / 代码块同步),无需复杂特性。
- 希望减少手动管理锁的风险(避免忘记释放锁导致死锁)。
- 依赖 JVM 自动优化(如偏向锁对单线程场景的优化)。
-
优先用
ReentrantLock
的场景:- 需要 公平锁(保证线程按等待顺序获取锁)。
- 需要 中断等待中的线程(如超时放弃或响应外部中断)。
- 需要 多个条件变量(如生产者 - 消费者模型中区分 "空""满" 条件)。
- 需要 查询锁状态(如判断锁是否被持有、当前线程重入次数)。