一、对象头与Mark Word的解剖:锁的"身份证"
每个Java对象在内存中都有个"身份证"------对象头,其中隐藏着锁的秘密。对象头中的Mark Word(标记字段)记录了锁状态、GC信息、哈希码等。
Mark Word的"七十二变"
- 无锁状态:存储对象的哈希码和分代年龄。
- 偏向锁:记录线程ID和偏向时间戳。
- 轻量级锁:指向栈中锁记录的指针。
- 重量级锁:指向操作系统互斥量(Mutex)的指针。
举个栗子 :
当线程A首次访问同步代码时,Mark Word会贴上A的"VIP标签"(偏向锁)。如果线程B来抢锁,VIP标签被撕掉,升级为轻量级锁,此时Mark Word变成线程B的栈锁记录指针。竞争激烈时,最终变成重量级锁,Mark Word指向操作系统级的互斥量。
工具推荐 :用JOL
(Java Object Layout)工具打印对象内存布局,直观测锁状态变化。
java
// 添加依赖:org.openjdk.jol:jol-core
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
二、锁升级的"通关秘籍":从偏向锁到重量级锁
锁升级不是"一刀切",而是根据竞争动态调整的智能策略。
-
偏向锁延迟 (Biased Locking Delay)
默认情况下,JVM启动后4秒才会启用偏向锁(通过参数
-XX:BiasedLockingStartupDelay=0
可关闭延迟)。这是为了避免启动时大量锁竞争导致频繁撤销偏向锁。 -
轻量级锁的"自旋赌局"
线程在轻量级锁状态下会通过自旋(空循环)尝试获取锁。
- 固定自旋次数:JDK 6之前默认10次。
- 自适应自旋:JDK 6之后,JVM根据前一次自旋的成功率动态调整次数。
-
重量级锁的"终极审判"
进入重量级锁后,未抢到锁的线程会被挂起,进入阻塞队列等待操作系统调度。此时性能开销最大,但能保证公平性。
锁升级触发条件:
- 偏向锁 → 轻量级锁:有第二个线程尝试获取锁。
- 轻量级锁 → 重量级锁:自旋失败(比如自旋次数用完,或第三个线程加入竞争)。
三、synchronized的字节码真相:monitorenter和monitorexit
编译后的synchronized
代码块会被翻译为两条指令:
- monitorenter:尝试获取锁。
- monitorexit:释放锁(无论正常结束还是异常退出)。
反编译看真相:
java
public void demo() {
synchronized (this) {
System.out.println("Hello");
}
}
用javap -c
反编译后:
yaml
public void demo();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 加锁
4: getstatic #2
7: ldc #3
9: invokevirtual #4
12: aload_1
13: monitorexit // 正常释放锁
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // 异常时释放锁(finally机制)
20: aload_2
21: athrow
22: return
注意 :monitorexit
会出现两次,确保锁在异常时也能释放!
四、逃逸分析与锁消除:JVM的"作弊优化"
JVM发现某些锁根本不需要时,会直接"拆掉锁",提升性能。
锁消除(Lock Elision):
java
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer();
synchronized(sb) { // 锁的是局部变量,且未逃逸 → 被JVM优化掉
sb.append(s1).append(s2);
}
return sb.toString();
}
由于sb
是局部变量且未逃逸出方法,JVM会直接删除同步代码,仿佛从未加锁!
逃逸分析(Escape Analysis) :
JVM通过分析对象的作用域,判断是否可以被优化(比如栈上分配、锁消除)。
五、synchronized与ReentrantLock的"全面战争"
特性 | synchronized | ReentrantLock |
---|---|---|
锁获取方式 | 自动获取和释放 | 必须手动lock() 和unlock() |
可中断性 | 不支持 | 支持lockInterruptibly() |
公平锁 | 非公平(默认) | 可选公平或非公平 |
条件变量 | 只能绑定一个wait/notify 队列 |
支持多个Condition |
性能 | JDK 6后优化后接近ReentrantLock | 高竞争时性能更好 |
选型建议:
- 简单场景用
synchronized
(代码简洁,不易出错)。 - 高竞争、需要超时/中断功能时选
ReentrantLock
。
六、死锁检测与破解:多线程的"密室逃脱"
经典死锁代码:
java
Object lockA = new Object();
Object lockB = new Object();
// 线程1
new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
}).start();
// 线程2
new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) { /* ... */ }
}
}).start();
如何破局?
- 统一加锁顺序:所有线程按固定顺序获取锁(比如先lockA后lockB)。
- 尝试加锁 :用
ReentrantLock.tryLock()
设置超时时间。 - 工具检测 :用
jstack
或VisualVM
查看线程dump,定位死锁链。
七、JVM调优参数:锁的"遥控器"
-
偏向锁优化:
-XX:+UseBiasedLocking
:启用偏向锁(默认开启)。-XX:BiasedLockingStartupDelay=0
:关闭偏向锁延迟。
-
自旋锁参数:
-XX:PreBlockSpin=10
:设置自旋次数(JDK 6后已废弃,自适应自旋接管)。
-
锁粗化(Lock Coarsening) :
JVM自动合并相邻的同步块,减少锁开销。
java// 优化前 synchronized (lock) { doA(); } synchronized (lock) { doB(); } // 优化后 synchronized (lock) { doA(); doB(); }
八、终极面试题:挑战年薪百万
-
synchronized能否锁住对象内部的属性修改?
(答:不能!锁的是对象实例,若直接修改对象属性,需确保所有访问路径都同步。)
-
静态同步方法和非静态同步方法是否互斥?
(答:不互斥!静态方法锁的是Class对象,非静态方法锁的是实例对象。)
-
synchronized是否可重入?为什么?
(答:可重入。每次获取锁时,JVM会记录持有锁的线程和进入次数,避免自己锁死自己。)
九、总结:synchronized的"终极奥义"
- 底层是Monitor:每个对象关联一个监视器,通过操作系统Mutex实现线程调度。
- 锁升级是核心优化:从偏向锁到重量级锁,兼顾性能和公平。
- JVM暗藏玄机:逃逸分析、锁消除、锁粗化,默默提升性能。
最后忠告:多线程编程如同走钢丝,synchronized是你的平衡杆------用得好稳如老狗,用不好"秃头"警告!
扩展阅读:
- JEP 374: 禁用偏向锁(JDK 15开始默认禁用偏向锁)
(想要成为锁的"驯龙高手"?赶紧动手写代码,用JOL
和jstack
探索锁的微观世界吧!)