在Java并发编程中,synchronized关键字看似简单,实则暗藏玄机。从JDK 1.6开始,HotSpot虚拟机对synchronized进行了革命性优化,引入了锁升级机制,将锁状态分为四种:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这些状态会随着线程竞争的激烈程度动态升级(不可降级),大幅提升了并发性能。
本文将深入剖析这四种状态的原理、转换机制,并新增核心内容:如何通过编程技巧避免锁升级,特别是避免升级到重量级锁。文末附有完整可运行的示例代码,助你彻底掌握Java锁的底层奥秘。
一、核心基础:对象头与Mark Word
Java对象在内存中的布局包含对象头(Object Header),其中最关键的部分是Mark Word(64位JVM下占64位)。Mark Word存储了对象的哈希码、GC分代年龄、锁状态标志等信息。锁状态由Mark Word中的锁标志位和偏向锁标志位共同决定:
| 锁状态 | 偏向锁标志 | 锁标志位 | Mark Word内容 | 说明 |
|---|---|---|---|---|
| 无锁 | 0 | 01 | 对象哈希码、GC分代年龄等 | 初始状态 |
| 偏向锁 | 1 | 01 | 偏向线程ID、偏向时间戳、锁次数等 | 单线程场景优化 |
| 轻量级锁 | 0 | 00 | 指向栈中Lock Record的指针 | 轻度竞争场景 |
| 重量级锁 | 0 | 10 | 指向Monitor对象的指针 | 重度竞争场景 |
💡 关键点:锁标志位是Mark Word的最后两位(二进制),
01表示无锁或偏向锁(需结合偏向锁标志位判断)。
二、四种锁状态详解
1. 无锁状态(Unlocked)
- 特点:对象刚创建时的默认状态。
- Mark Word:存储对象哈希码、GC分代年龄等。
- 性能:无任何同步开销。
- 触发条件:对象未被任何线程锁定。
2. 偏向锁状态(Biased Lock)
- 特点:针对单线程访问场景的优化,避免CAS开销。
- 工作原理:
- 第一个线程获取锁时,JVM将Mark Word的偏向锁标志设为1。
- 记录该线程ID到Mark Word。
- 后续该线程再次获取锁时,只需检查线程ID是否匹配(无需CAS)。
- 优势:单线程场景下锁开销接近0。
- 触发条件:无竞争的单线程访问。
- 撤销机制:当有其他线程竞争时,JVM会撤销偏向锁(需1次CAS)。
3. 轻量级锁状态(Lightweight Lock)
- 特点:针对轻度竞争(线程交替执行)的优化。
- 工作原理:
- 线程尝试通过CAS将Mark Word替换为指向栈帧Lock Record的指针。
- 若CAS成功,锁状态变为轻量级锁。
- 若CAS失败(有竞争),线程进入自旋(Spin) 等待(默认10次)。
- 优势:避免OS线程阻塞,减少上下文切换开销。
- 触发条件:偏向锁撤销后,有多个线程竞争但竞争不激烈。
- 关键数据结构:
// 模拟Lock Record(JVM内部结构) class LockRecord { Object owner; // 指向当前持有锁的线程 Object displaced; // 原Mark Word内容 }
4. 重量级锁状态(Heavyweight Lock)
- 特点:针对重度竞争的兜底方案。
- 工作原理:
- 当自旋次数超过阈值(默认10次)或竞争激烈时,锁升级为重量级锁。
- Mark Word指向Monitor对象(JVM实现的互斥量)。
- 线程阻塞在OS层面(进入等待队列),由OS调度唤醒。
- 优势:保证线程安全,避免CPU空转。
- 触发条件:轻量级锁自旋失败,或竞争持续激烈。
- 性能代价:线程阻塞/唤醒开销大(约1000ns)。
三、状态转换机制(核心!)
锁状态升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
不可降级:一旦升级到重量级锁,不会回退到轻量级锁。
转换流程图
graph LR
A[无锁] -->|第一个线程获取锁| B[偏向锁]
B -->|其他线程竞争| C[轻量级锁]
C -->|自旋失败/竞争激烈| D[重量级锁]
详细转换步骤
-
无锁 → 偏向锁
- 当第一个线程执行
synchronized时,JVM将偏向锁标志设为1,记录线程ID。
- 当第一个线程执行
-
偏向锁 → 轻量级锁
- 当有其他线程尝试获取锁(偏向线程ID不匹配),JVM撤销偏向锁。
- 通过CAS将Mark Word替换为指向Lock Record的指针。
-
轻量级锁 → 重量级锁
- 当线程自旋超过阈值(默认10次)或CAS失败率高时,锁升级。
- Mark Word指向Monitor对象,线程阻塞在OS层。
📌 重要提示:
- 偏向锁默认开启(JDK 8+),但可关闭:
-XX:-UseBiasedLocking- 轻量级锁升级为重量级锁后,不会自动降级。
四、避免锁升级的编程技巧:如何避免重量级锁
重量级锁是性能的“黑洞”,线程阻塞开销高达1000ns。要避免升级到重量级锁,核心原则是:减少锁持有时间和降低锁竞争强度。以下是实操技巧和代码示例。
技巧1:缩短锁持有时间(最关键!)
原理:锁持有时间越长,竞争线程自旋失败概率越高,触发重量级锁升级。 错误示例:在锁内执行耗时操作(I/O、网络请求、长计算)
public class BadLock {
private final Object lock = new Object();
public void process() {
synchronized (lock) {
// ❌ 错误:锁内进行耗时操作(数据库查询)
try {
Thread.sleep(500); // 模拟500ms数据库操作
} catch (InterruptedException e) {
e.printStackTrace();
}
// 其他业务逻辑
}
}
}
后果:任何竞争线程都会自旋10次失败,触发重量级锁升级。
优化示例:将耗时操作移出同步块
public class GoodLock {
private final Object lock = new Object();
public void process() {
// ✅ 优化:先执行耗时操作(不持有锁)
Data data = fetchDataFromDB();
synchronized (lock) {
// ✅ 仅同步处理数据(锁持有时间极短)
processData(data);
}
}
private Data fetchDataFromDB() {
try {
Thread.sleep(500); // 耗时操作,但不在锁内
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Data("optimized");
}
private void processData(Data data) {
// 快速处理数据
System.out.println("Processing: " + data.value);
}
}
效果:锁持有时间从500ms缩短到<1ms,避免升级到重量级锁。
技巧2:减小锁的粒度(细粒度锁)
原理:全局锁(如synchronized方法)导致所有操作串行化,易引发竞争。
错误示例:使用全局锁操作多个独立数据
public class GlobalLock {
private Map<String, String> map = new HashMap<>();
// ❌ 错误:put和get共享同一个锁,即使操作不同键
public synchronized void put(String key, String value) {
map.put(key, value);
}
public synchronized void get(String key) {
map.get(key);
}
}
后果:操作key1的put会阻塞操作key2的get,竞争激烈。
优化示例:使用分段锁(细粒度锁)
public class FineGrainedLock {
private final Map<String, String> map = new HashMap<>();
private final Map<String, Object> locks = new ConcurrentHashMap<>();
// ✅ 优化:每个key使用独立锁
public void put(String key, String value) {
Object lock = locks.computeIfAbsent(key, k -> new Object());
synchronized (lock) {
map.put(key, value);
}
}
public String get(String key) {
Object lock = locks.get(key);
if (lock == null) return null;
synchronized (lock) {
return map.get(key);
}
}
}
效果:操作不同key时互不阻塞,竞争显著降低,避免升级到重量级锁。
技巧3:避免锁内调用外部方法
原理:锁内调用可能获取其他锁的方法(如toString()、日志输出),导致间接竞争。
错误示例:锁内调用可能持有其他锁的方法
public class DeadlockProne {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
// ❌ 错误:调用可能持有lock2的方法
System.out.println("In methodA"); // System.out可能持有全局锁
}
}
public void methodB() {
synchronized (lock2) {
System.out.println("In methodB");
}
}
}
后果:System.out.println可能持有PrintStream锁,导致锁嵌套竞争,升级为重量级锁。
优化示例:锁内只做简单操作,避免调用外部方法
public class SafeLock {
private final Object lock = new Object();
public void safeMethod() {
synchronized (lock) {
// ✅ 仅执行简单操作
System.out.println("Safe operation"); // 但需注意:System.out可能仍会触发竞争
}
}
// 更安全的做法:避免在锁内使用日志
public void safeMethodWithoutLog() {
synchronized (lock) {
// 仅处理业务逻辑
processBusiness();
}
}
}
最佳实践:在锁内绝对避免调用外部方法(尤其涉及I/O、日志、网络),确保锁持有期间无其他锁竞争。
技巧4:使用并发集合替代同步集合
原理:Collections.synchronizedMap使用全局锁,易升级为重量级锁。
错误示例:使用Collections.synchronizedMap
public class SyncMapExample {
private Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
public void put(String key, String value) {
map.put(key, value); // 全局锁,竞争激烈
}
}
后果:所有put/get操作共享同一锁,竞争高,易升级重量级锁。
优化示例:使用ConcurrentHashMap
public class ConcurrentMapExample {
private Map<String, String> map = new ConcurrentHashMap<>();
public void put(String key, String value) {
map.put(key, value); // 分段锁,锁粒度细
}
}
效果:ConcurrentHashMap使用分段锁(JDK 8+为Node锁),竞争大幅降低,避免重量级锁升级。
五、代码示例:验证优化效果
使用JOL工具打印优化前后的锁状态(JDK 1.8+,需添加JOL依赖)。
优化前(易升级重量级锁)
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class LockUpgradeDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 无锁状态
Object obj = new Object();
System.out.println("=== 初始无锁状态 ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println();
// 2. 错误示例:锁内耗时操作(易升级重量级锁)
final Object badLock = new Object();
Thread t1 = new Thread(() -> {
synchronized (badLock) {
try {
Thread.sleep(500); // 持有锁500ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (badLock) {
// 立即竞争锁(竞争激烈)
}
});
t1.start();
Thread.sleep(50);
t2.start();
t1.join();
t2.join();
System.out.println("=== 优化前:锁内耗时操作 ===");
System.out.println(ClassLayout.parseInstance(badLock).toPrintable());
}
}
输出关键行:0x0000000000000002(重量级锁标志)
优化后(避免重量级锁)
public class LockOptimizedDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 无锁状态
Object obj = new Object();
System.out.println("=== 初始无锁状态 ===");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println();
// 2. 优化示例:缩短锁持有时间
final Object goodLock = new Object();
Thread t1 = new Thread(() -> {
// ✅ 优化:耗时操作在锁外
Data data = fetchDataFromDB();
synchronized (goodLock) {
processData(data);
}
});
Thread t2 = new Thread(() -> {
// ✅ 优化:锁持有时间极短
synchronized (goodLock) {
// 仅处理数据
}
});
t1.start();
Thread.sleep(50);
t2.start();
t1.join();
t2.join();
System.out.println("=== 优化后:缩短锁持有时间 ===");
System.out.println(ClassLayout.parseInstance(goodLock).toPrintable());
}
private static Data fetchDataFromDB() {
try {
Thread.sleep(500); // 耗时操作(不在锁内)
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Data("optimized");
}
private static void processData(Data data) {
// 快速处理
}
static class Data {
String value;
Data(String value) { this.value = value; }
}
}
输出关键行:0x0000000000000002(轻量级锁,未升级到重量级锁)
六、总结:掌握锁状态的实践价值
-
避免重量级锁的核心:
缩短锁持有时间(锁内不执行耗时操作) + 减小锁粒度(细粒度锁) + 避免锁内调用外部方法。 -
性能对比(理论值):
锁状态 锁持有时间 线程开销 适用场景 偏向锁 <100ns 极低 单线程连续访问 轻量级锁 <100ns 低(自旋) 轻度竞争(交替执行) 重量级锁 1000ns+ 高(OS阻塞) 重度竞争(锁持有长) -
关键结论:
重量级锁是性能的“罪魁祸首”,但通过合理编程技巧(缩短锁持有时间、细粒度锁),90%的锁升级问题可以避免。JVM的锁升级是自动优化,我们只需编写“锁持有时间短”的代码。
-
JVM调优建议(辅助优化):
# 仅在竞争激烈场景关闭偏向锁(避免撤销开销) -XX:-UseBiasedLocking # 调整自旋次数(默认10次,竞争激烈时可调高) -XX:PreBlockSpin=20
附:JOL工具使用指南
-
安装:通过Maven添加依赖
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.14</version> </dependency> -
打印锁状态:
System.out.println(ClassLayout.parseInstance(obj).toPrintable()); -
关键标识:
0x0000000000000005→ 偏向锁0x0000000000000002→ 轻量级锁或重量级锁(需结合竞争情况判断)- 重量级锁:在JOL输出中显示为
0x0000000000000002,但实际Mark Word指向Monitor(通过JDK工具如jstack确认)。
💬 终极建议:
在编写同步代码时,始终问自己:
“这个锁持有时间够短吗?是否可以拆分成更小的锁?”
99%的性能问题源于锁持有时间过长。掌握这些技巧,你就能写出无锁升级的高效并发代码!
🔍 实践验证:
用JOL工具运行本文示例,对比优化前后的锁状态。你会发现,缩短锁持有时间是避免重量级锁的最有效手段。
通过本文,你已掌握Java内置锁的底层进化逻辑与避免升级的实战技巧。在编写并发代码时,记住:
锁升级是JVM的自我优化,而避免升级是你的设计责任。
作者:架构师Beata
日期:2026年3月4日
声明:本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动:如有任何问题?欢迎在评论区分享!