托拉姆物语
62.6MB · 2026-03-07
在Java并发编程中,synchronized关键字看似简单,实则暗藏玄机。从JDK 1.6开始,HotSpot虚拟机对synchronized进行了革命性优化,引入了锁升级机制,将锁状态分为四种:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这些状态会随着线程竞争的激烈程度动态升级(不可降级),大幅提升了并发性能。
本文将深入剖析这四种状态的原理、转换机制,并新增核心内容:如何通过编程技巧避免锁升级,特别是避免升级到重量级锁。文末附有完整可运行的示例代码,助你彻底掌握Java锁的底层奥秘。
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对象的指针 | 重度竞争场景 |
// 模拟Lock Record(JVM内部结构)
class LockRecord {
Object owner; // 指向当前持有锁的线程
Object displaced; // 原Mark Word内容
}
锁状态升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
不可降级:一旦升级到重量级锁,不会回退到轻量级锁。
graph LR
A[无锁] -->|第一个线程获取锁| B[偏向锁]
B -->|其他线程竞争| C[轻量级锁]
C -->|自旋失败/竞争激烈| D[重量级锁]
无锁 → 偏向锁
synchronized时,JVM将偏向锁标志设为1,记录线程ID。偏向锁 → 轻量级锁
轻量级锁 → 重量级锁
重量级锁是性能的“黑洞”,线程阻塞开销高达1000ns。要避免升级到重量级锁,核心原则是:减少锁持有时间和降低锁竞争强度。以下是实操技巧和代码示例。
原理:锁持有时间越长,竞争线程自旋失败概率越高,触发重量级锁升级。 错误示例:在锁内执行耗时操作(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,避免升级到重量级锁。
原理:全局锁(如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时互不阻塞,竞争显著降低,避免升级到重量级锁。
原理:锁内调用可能获取其他锁的方法(如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、日志、网络),确保锁持有期间无其他锁竞争。
原理: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阻塞) | 重度竞争(锁持有长) |
关键结论:
JVM调优建议(辅助优化):
# 仅在竞争激烈场景关闭偏向锁(避免撤销开销)
-XX:-UseBiasedLocking
# 调整自旋次数(默认10次,竞争激烈时可调高)
-XX:PreBlockSpin=20
安装:通过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 → 轻量级锁或重量级锁(需结合竞争情况判断)0x0000000000000002,但实际Mark Word指向Monitor(通过JDK工具如jstack确认)。通过本文,你已掌握Java内置锁的底层进化逻辑与避免升级的实战技巧。在编写并发代码时,记住:
锁升级是JVM的自我优化,而避免升级是你的设计责任。