
synchronized原理
Monitor 概念回顾
Java对象头
以 32 位虚拟机为例
名词解释:
Object Header (64 bits): 它包含了对象的元信息以及用于垃圾回收和同步的数据。
Mark Word (32 bits): 这个部分通常包含用于垃圾回收和同步的标记信息。标记字包含了对象的哈希码、锁定状态、垃圾回收标记等信息。
Klass Word (32 bits) : 这个部分包含指向对象的类元数据的指针,它描述了对象属于哪个类,包括类的方法、字段等信息。
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
hashcode:这部分通常用于存储对象的哈希码(HashCode),它是一个用于快速查找对象的值。哈希码在对象创建时生成,然后在对象的生命周期中不会更改。
age (4 bits): 这部分用于表示对象的年龄,通常在分代垃圾回收中使用。年龄是一个对象存活的时间的度量。
biased_lock (1 bit): 这个位用于标识对象是否启用了偏向锁。当偏向锁被启用时,该位为1;否则,为0。
01 State: 这个状态表示对象处于正常状态,未被锁定或标记。
thread (23 bits): 这一部分用于存储拥有锁的线程的ID。在偏向锁状态下,它表示偏向锁的线程ID。
epoch (2 bits): 这一部分用于存储偏向时间戳(bias timestamp)。它用于检测是否应取消偏向锁。在不同时间偏向锁的情况下,该值可能不同。
age (4 bits): 这一部分用于表示对象的年龄,通常在分代垃圾回收中使用。年龄是一个对象存活的时间的度量。
biased_lock (1 bit): 这个位用于标识对象是否启用了偏向锁。当偏向锁被启用时,该位为1。
01 State: 这个状态表示对象处于偏向锁状态,已经偏向某个线程。
ptr_to_lock_record (30 bits): 这一部分用于指向偏向锁的记录(bias lock record),该记录包含了关于偏向锁的详细信息。这在取消偏向锁时使用。
00 State: 这个状态表示对象处于轻量级锁状态。
ptr_to_heavyweight_monitor (30 bits): 这一部分用于指向重量级锁的监视器对象。重量级锁通常涉及多个线程之间的同步。
10 State: 这个状态表示对象处于重量级锁状态。
11 State: 这个状态表示对象被标记为垃圾回收(GC Marked),通常在垃圾回收期间使用。
64位虚拟机 Mark Word
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal 无锁状态 |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased 偏向锁 |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked 轻量级锁|
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked 重量级锁|
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
synchronized加锁过程
无锁状态(Unlocked):一开始,对象处于无锁状态。这意味着没有线程持有该对象的锁。
偏向锁检测(Biased Lock Check):在进入同步块之前,JVM 会尝试检测对象是否已经偏向某个线程。偏向锁是一种优化,旨在减少不必要的竞争。如果对象已经被偏向某个线程,且当前线程是偏向线程,那么它可以直接进入同步块,跳过后续步骤。
自旋锁(Spin Locking):如果对象不是偏向任何线程,或者当前线程不是偏向线程,JVM 会尝试使用自旋锁来避免进入重量级锁。自旋锁是一种快速的锁获取尝试,线程会短暂自旋等待锁的释放,而不进入阻塞状态。
轻量级锁尝试(Lightweight Lock Attempt):如果自旋锁不成功,当前线程将尝试使用轻量级锁。此时,JVM会尝试在对象头中的
Mark Word
中设置标志来表示当前线程持有该对象的锁。竞争(Contention):如果轻量级锁尝试失败,表示可能有其他线程也在竞争同一个锁,进入竞争状态。这时,JVM 将使用适当的机制来处理竞争,通常会升级锁为重量级锁。
重量级锁(Heavyweight Lock):如果竞争仍然存在,JVM 将升级锁为重量级锁。重量级锁使用操作系统的原生同步机制,例如互斥量,来确保同一时刻只有一个线程可以进入同步块。其他线程将被阻塞,直到持有锁的线程释放它。
执行同步块(Executing Synchronized Block):一旦线程成功获取锁,它可以进入同步块内执行相应的代码。只有一个线程可以同时执行同步块内的代码。
释放锁(Release Lock):当线程退出同步块或抛出异常时,它会释放锁,允许其他线程竞争该锁。
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
如果 cas 失败,有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象 头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
自旋重试失败的情况
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能,由操作系统底层控制
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。
以后只要不发生竞争,这个对象就归该线程所有 例如:
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
偏向状态
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased 偏向锁 |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值
测试偏向锁延迟特性
默认synchronized
加锁是先加的偏向锁
首先配置: -XX:BiasedLockingStartupDelay=0
禁用延迟,如果不禁用,请用sleep方法让主线程睡不低于4s,因为这是偏向锁的特性,延迟加载
代码
public static void test3() {
A a = new A();
ClassLayout classLayout = ClassLayout.parseInstance(a);
new Thread(() -> {
log.debug("加锁前:{}", classLayout.toPrintableSimpleSerMs());
synchronized (a) {
log.debug("加锁之后:{}", classLayout.toPrintableSimpleSerMs());
}
log.debug("解锁后:{}", classLayout.toPrintableSimpleSerMs());
}, "T1").start();
}
打印结果
第一次打印锁状态为
101
为偏向锁状态第二次对A对象进行加锁操作,最后三位可以看到还是
101
依然是偏向锁,不同的是后面的54为多了T1线程的ThreadId第三次解锁之后打印的结果跟第二次一样,这也就是偏向锁的思想,偏向锁的对象解锁后,线程 id 仍存储于对象头中
上述的toPrintableSimpleSerMs()
方法是基于Jol-core Jar包进行的扩展方法,详细可以看这篇文章
{% link https://bu.dusays.com/2023/10/11/65269ea6226c8.png , Java扩展第三方Jar , https://blog.serms.top/post/8388fdb4/ %}
测试禁用偏向锁
在运行配置中设置VM Options
-XX:-UseBiasedLocking
打印结果
因禁用了偏向锁,所以第一次打印的最后三位为
001
处于无锁的状态第二次是加锁,最后三位为
000
偏向锁被禁用了,只能升级为轻量级锁,前面54为依然为ThreadId
第三次解锁之后,回到无锁状态
001
测试HashCode
运行配置改成取消延迟加载
public static void test3() {
A a = new A();
ClassLayout classLayout = ClassLayout.parseInstance(a);
new Thread(() -> {
log.debug("加锁前:{}", classLayout.toPrintableSimpleSerMs());
a.hashCode();
synchronized (a) {
log.debug("加锁之后:{}", classLayout.toPrintableSimpleSerMs());
}
log.debug("解锁后:{}", classLayout.toPrintableSimpleSerMs());
}, "T1").start();
}
打印结果
第一次后三位为
101
,偏向锁状态第二次加锁后升级为
轻量级锁
,后面的 62位记录的是偏向锁的详细信息,在取消加锁的时候会用到第三次取消加锁之后,后三位为
001
无锁状态,前面的为HashCode值
- 感谢你赐予我前进的力量