synchronized与锁升级
锁升级概述
synchronized锁升级是JVM为了优化同步性能而设计的机制。锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
核心思想
- 先自旋,不行再阻塞
- 根据竞争程度动态调整锁的实现方式
- 避免不必要的重量级锁开销
多线程访问情况分类
1. 单线程访问
- 场景:只有一个线程来访问,有且唯一Only One
- 适用锁:偏向锁
- 特点:性能最优,几乎无额外开销
2. 多线程交替访问
- 场景:有多个线程(2个线程A、B来交替访问)
- 适用锁:轻量级锁
- 特点:通过CAS自旋避免阻塞
3. 多线程激烈竞争
- 场景:竞争激烈,更多个线程来访问
- 适用锁:重量级锁
- 特点:线程阻塞,由操作系统调度
锁升级流程
synchronized用的锁是存在Java对象头里的Mark Word中,锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
锁指向关系
- 偏向锁:MarkWord存储的是偏向的线程ID
- 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
- 重量锁:MarkWord存储的是指向堆中的monitor对象的指针
各种锁状态详解
1. 无锁状态 (001)
概念
无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)
特点
- 对象刚创建时的初始状态
- Mark Word可以存储identity hash code
- 最后三位标志:001
2. 偏向锁状态 (101)
概念
偏向锁:单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
主要作用
当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁
设计理念
Hotspot的作者经过研究发现,大多数情况下:
- 多线程的情况下,锁不仅不存在多线程竞争
- 还存在锁由同一个线程多次获得的情况
- 偏向锁就是在这种情况下出现的,为了解决只有在一个线程执行同步时提高性能
偏向锁的持有机制
理论落地: 在实际应用运行过程中发现,"锁总是同一个线程持有,很少发生竞争",也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
工作流程:
- 锁第一次被拥有时,记录下偏向线程ID
- 偏向线程进入和退出同步代码块时,不需要再次加锁和释放锁
- 直接检查锁的MarkWord里面是不是放的自己的线程ID
检查结果:
- 如果相等:表示偏向锁是偏向于当前线程的,直接进入同步,无需CAS操作
- 如果不等 :表示发生了竞争,尝试使用CAS替换MarkWord里面的线程ID
- 竞争成功:MarkWord更新为新线程ID,锁不升级,仍然为偏向锁
- 竞争失败:需要升级为轻量级锁
注意:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
偏向锁的撤销
当有另外线程来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁。
撤销时机: 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁
撤销机制: 偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
- 第一个线程正在执行synchronized方法 :偏向锁会被取消并出现锁升级,轻量级锁由原持有偏向锁的线程持有,竞争线程进入自旋等待
- 第一个线程执行完成synchronized方法:将对象头设置成无锁状态并撤销偏向锁,重新偏向
重要提示:Java15逐步废弃偏向锁
3. 轻量级锁状态 (00)
概念
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS
轻量级锁的获取
设计目标: 轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。
升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
Lock Record详解
Lock Record是什么? Lock Record是线程私有的数据结构,位于线程的栈帧中,用于存储锁对象的Mark Word拷贝以及owner指针。
Lock Record结构:
diff
+------------------+
| Displaced Mark | <- 存储锁对象原始的Mark Word
| Word |
+------------------+
| Owner Pointer | <- 指向锁对象的指针
+------------------+
Lock Record工作机制:
-
创建Lock Record:线程尝试获取轻量级锁时,首先在当前线程的栈帧中创建Lock Record
-
拷贝Mark Word:将锁对象的Mark Word拷贝到Lock Record的Displaced Mark Word字段中
-
CAS操作:使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针
- 成功:获取锁成功,锁对象的Mark Word指向当前线程的Lock Record
- 失败:说明有竞争,可能需要自旋或升级为重量级锁
-
释放锁:使用CAS操作将Displaced Mark Word的值恢复到锁对象的Mark Word中
- 成功:释放锁成功
- 失败:说明锁已经升级为重量级锁,需要唤醒被阻塞的线程
获取流程: 假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
-
如果锁获取成功:直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程,该锁会保持偏向锁状态,A线程Over,B线程上位
-
如果锁获取失败:偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁
自旋机制
Java6之前:
- 默认启用
- 默认情况下自旋的次数是10次
- 或者自旋线程数超过cpu核数一半
Java6之后: 自适应自旋锁
自适应意味着自旋的次数不是固定不变的,而是根据:
- 同一个锁上一次自旋的时间
- 拥有锁线程的状态来决定
轻量锁与偏向锁的区别
- 争夺轻量级锁失败时,自旋尝试抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
4. 重量级锁状态 (10)
概念
有大量的线程参与锁的竞争,冲突性很高
轻量级锁升级为重量级锁的详细流程
触发时机:
-
自旋超过限制:
- Java 6之前:自旋次数超过10次
- Java 6之后:自适应自旋失败(根据历史自旋成功率动态调整)
- 自旋线程数:超过CPU核数的一半
-
竞争激烈:
- 同时有多个线程竞争同一个锁
- 锁持有时间较长,自旋等待不划算
升级流程详解:
第一步:检测升级条件
css
线程A持有轻量级锁 → 线程B自旋等待 → 线程C也来竞争
↓
检测到多线程竞争激烈
↓
触发锁升级条件
第二步:创建Monitor对象
- JVM在堆中创建ObjectMonitor对象
- ObjectMonitor包含:
_owner
:指向持有锁的线程_EntryList
:等待获取锁的线程队列_WaitSet
:调用wait()方法的线程队列_count
:重入次数计数器
第三步:转换过程
- 暂停持有锁的线程:在安全点暂停线程A
- 创建Monitor:在堆中分配ObjectMonitor对象
- 设置Monitor状态 :
- 将线程A设置为Monitor的owner
- 将竞争线程B、C加入到EntryList中
- 更新对象头:将Mark Word更新为指向Monitor对象的指针
- 唤醒线程:恢复线程A的执行,线程B、C进入阻塞状态
第四步:重量级锁运行机制
css
线程A (owner) 执行同步代码
↓
线程A 释放锁 (monitorexit)
↓
从EntryList中唤醒一个等待线程
↓
被唤醒的线程成为新的owner
重量级锁原理
Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。
当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。
ObjectMonitor结构详解
cpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次数
_waiters = 0; // 等待线程数
_recursions = 0; // 重入计数
_object = NULL; // 监视器锁寄生的对象
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 调用wait后,线程会被加入到_WaitSet
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL; // 多线程竞争锁时的单向链表
FreeNext = NULL;
_EntryList = NULL; // _cxq队列中有资格成为候选资源的线程会被移动到该队列
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
重量级锁的获取和释放流程
获取流程:
- 检查owner:如果owner为null,通过CAS设置当前线程为owner
- 检查重入:如果owner是当前线程,增加重入计数
- 加入等待队列:如果获取失败,加入EntryList或cxq队列
- 阻塞等待:调用park()方法阻塞当前线程
- 被唤醒后重试:被unpark()唤醒后重新尝试获取锁
释放流程:
- 检查owner:确认当前线程是锁的持有者
- 处理重入:如果有重入,减少重入计数
- 释放锁:将owner设置为null
- 唤醒等待线程:从EntryList或cxq中选择一个线程唤醒
为什么要升级为重量级锁?
自旋的代价:
- CPU消耗:自旋会持续消耗CPU资源
- 缓存污染:多个线程在同一个缓存行上自旋会导致缓存失效
- 不公平性:自旋可能导致某些线程长时间得不到锁
重量级锁的优势:
- 节省CPU:阻塞线程不消耗CPU资源
- 公平性:通过队列保证一定的公平性
- 适合长时间持锁:避免无意义的自旋等待
对象头Mark Word结构
64位JVM中Mark Word结构
锁状态 | 64位Mark Word结构 | 锁标志位 | 偏向锁标志 | 整体后3位 |
---|---|---|---|---|
无锁 | unused:25bit | identity_hashcode:31bit | unused:1bit | age:4bit | 0 | 01 | 0 | 001 |
偏向锁 | thread:54bit | epoch:2bit | unused:1bit | age:4bit | 1 | 01 | 1 | 101 |
轻量级锁 | ptr_to_lock_record:62bit | 00 | 无意义 | 00 |
重量级锁 | ptr_to_heavyweight_monitor:62bit | 10 | 无意义 | 10 |
GC标记 | 空 | 11 | 无意义 | 11 |
锁升级后hashcode去哪了?
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码,GC年龄了,那么这些信息被移动到哪里去了呢?
- 在无锁状态下:Mark Word中可以存储对象的identity hash code值
- 对于偏向锁:在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁
- 升级为轻量级锁时:JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code
- 升级为重量级锁后:Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word
锁升级代码示例
环境准备
首先需要添加JOL依赖(Maven):
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
完整示例代码
java
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class SynchronizationUpgradeProcess {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
System.out.println("======= 初始状态: 无锁 =======");
printObjectHeader(lock);
// 偏向锁演示
System.out.println("\n======= 阶段1: 偏向锁 =======");
demonstrateBiasedLocking();
TimeUnit.SECONDS.sleep(1);
// 轻量级锁演示
System.out.println("\n======= 阶段2: 轻量级锁 =======");
demonstrateLightweightLocking();
TimeUnit.SECONDS.sleep(1);
// 重量级锁演示
System.out.println("\n======= 阶段3: 重量级锁 =======");
demonstrateHeavyweightLocking();
}
private static void printObjectHeader(Object obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
private static void demonstrateBiasedLocking() throws Exception {
synchronized (lock) {
System.out.println("第一个线程获取锁 - 偏向锁状态:");
printObjectHeader(lock);
}
}
private static void demonstrateLightweightLocking() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1获取锁 (轻量级锁):");
printObjectHeader(lock);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(100);
synchronized (lock) {
System.out.println("线程2获取锁 (轻量级锁):");
printObjectHeader(lock);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
private static void demonstrateHeavyweightLocking() throws InterruptedException {
// 创建多个线程竞争锁
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获取锁 (重量级锁):");
printObjectHeader(lock);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "竞争线程-" + i).start();
Thread.sleep(50); // 确保线程按顺序启动
}
}
}
锁升级触发时机详解
1. 无锁 → 偏向锁
- 触发时机:第一个线程访问同步块
- 条件:JVM启用偏向锁且偏向锁延迟时间已过
- 特征:对象头最后3位从001变为101
2. 偏向锁 → 轻量级锁
- 触发时机:第二个线程尝试获取已被偏向的锁
- 条件:原持有线程已释放锁或CAS竞争失败
- 特征:对象头最后2位从01变为00
3. 轻量级锁 → 重量级锁
- 触发时机:自旋超过阈值或多线程激烈竞争
- 条件:自旋次数达到限制或竞争线程过多
- 特征:对象头最后2位从00变为10
JOL输出解析示例
输出格式说明
JOL(Java Object Layout)工具输出的对象头信息包含:
- OFFSET:字节偏移量
- SIZE:字段大小(字节)
- TYPE:字段类型
- DESCRIPTION:字段描述
- VALUE:十六进制值
各锁状态的JOL输出分析
1. 无锁状态输出:
css
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) e5 01 00 f8
12 4 (loss due to the next object alignment)
分析流程:
- 第一行(0-3字节) :
01 00 00 00
→ 小端序转换为00 00 00 01
- 最后3位:
001
→ 无锁状态 - 可存储identity hash code、GC分代年龄等信息
- 最后3位:
- 第二行(4-7字节) :
00 00 00 00
→ Mark Word的高32位 - 第三行(8-11字节) :
e5 01 00 f8
→ 类型指针(Klass pointer)
2. 偏向锁状态输出:
css
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00
4 4 (object header) f0 21 10 00
8 4 (object header) e5 01 00 f8
12 4 (loss due to the next object alignment)
分析流程:
- 第一行(0-3字节) :
05 00 00 00
→ 小端序转换为00 00 00 05
- 最后3位:
101
→ 偏向锁状态 - 偏向锁标志位为1,锁标志位为01
- 最后3位:
- 第二行(4-7字节) :
f0 21 10 00
→ 包含线程ID和epoch信息- 存储了偏向线程的ID(54位)
- epoch值(2位)用于批量重偏向
3. 轻量级锁状态输出:
css
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) f8 f1 5f 1a
4 4 (object header) 00 70 00 00
8 4 (object header) e5 01 00 f8
12 4 (loss due to the next object alignment)
分析流程:
- 第一行(0-3字节) :
f8 f1 5f 1a
→ 小端序转换- 最后2位:
00
→ 轻量级锁状态 - 高62位存储指向Lock Record的指针
- 最后2位:
- 第二行(4-7字节) :
00 70 00 00
→ Lock Record指针的高32位 - 指针解析:完整的Lock Record指针指向线程栈中的锁记录
4. 重量级锁状态输出:
css
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a 03 00 00
4 4 (object header) 70 ff 1f 00
8 4 (object header) e5 01 00 f8
12 4 (loss due to the next object alignment)
分析流程:
- 第一行(0-3字节) :
4a 03 00 00
→ 小端序转换- 最后2位:
10
→ 重量级锁状态 - 高62位存储指向ObjectMonitor的指针
- 最后2位:
- 第二行(4-7字节) :
70 ff 1f 00
→ Monitor指针的高32位 - 指针解析:完整的Monitor指针指向堆中的ObjectMonitor对象
锁状态识别技巧
锁状态 | 识别方法 | 关键特征 |
---|---|---|
无锁 | 查看最后3位 | 001 |
偏向锁 | 查看最后3位 | 101 |
轻量级锁 | 查看最后2位 | 00 |
重量级锁 | 查看最后2位 | 10 |
GC标记 | 查看最后2位 | 11 |
实际分析步骤
- 提取Mark Word:取前8个字节(64位系统)
- 小端序转换:将字节序调整为正确顺序
- 二进制转换:转换为二进制查看标志位
- 状态判断:根据最后2-3位确定锁状态
- 内容解析:根据锁状态解析Mark Word中的具体内容
性能对比与总结
各种锁优缺点对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
synchronized锁升级过程总结
一句话:就是先自旋,不行再阻塞。
实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式。
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
JDK版本演进:
- JDK1.6之前:synchronized使用的是重量级锁
- JDK1.6之后:进行了优化,拥有了无锁→偏向锁→轻量级锁→重量级锁的升级过程
适用场景:
- 偏向锁:适用于单线程使用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁
- 轻量级锁:适用于竞争较不激烈的情况,存在竞争时升级为轻量级锁,采用自旋锁,如果同步方法/代码块执行时间很短,采用轻量级锁虽然会占用cpu资源但相对比使用重量级锁还是更高效
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁
重要提示
-
Java 15开始偏向锁被废弃:因为现代应用通常有更多线程竞争的场景,偏向锁带来的收益较小,而撤销偏向锁的开销较大
-
锁升级是单向的:锁只能从低级别向高级别升级,不能降级
-
对象头信息迁移:锁升级后,原Mark Word中的信息(如hashCode、GC年龄)会被保存到其他位置
-
性能调优建议:
- 减少锁的持有时间
- 降低锁的粒度
- 避免不必要的同步
- 合理使用并发工具类
通过理解synchronized锁升级机制,我们可以更好地编写高性能的并发程序,在保证线程安全的同时最大化程序性能。