Monitor 概念回顾
Java对象头
以 32 位虚拟机为例
名词解释:
- Object Header (64 bits): 它包含了对象的元信息以及用于垃圾回收和同步的数据。
- Mark Word (32 bits): 这个部分通常包含用于垃圾回收和同步的标记信息。标记字包含了对象的哈希码、锁定状态、垃圾回收标记等信息。
- Klass Word (32 bits) : 这个部分包含指向对象的类元数据的指针,它描述了对象属于哪个类,包括类的方法、字段等信息。
普通对象
1 2 3 4 5
| |--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------|
|
数组对象
1 2 3 4 5
| |---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------|
|
其中 Mark Word 结构为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| |-------------------------------------------------------|--------------------| | 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
1 2 3 4 5 6 7 8 9 10 11 12 13
| |--------------------------------------------------------------------|--------------------| | 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
假设有两个方法同步块,利用同一个对象加锁
1 2 3 4 5 6 7 8 9 10 11 12
| static final Object obj = new Object(); public static void method1() { synchronized( obj ) { method2(); } } public static void method2() { synchronized( obj ) { } }
|
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
1 2 3 4 5 6
| static Object obj = new Object(); public static void method1() { synchronized( obj ) { } }
|
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
自旋重试失败的情况
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能,由操作系统底层控制
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。
以后只要不发生竞争,这个对象就归该线程所有 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static final Object obj = new Object(); public static void m1() { synchronized( obj ) { m2(); } } public static void m2() { synchronized( obj ) { m3(); } } public static void m3() { synchronized( obj ) { } }
|
偏向状态
1 2 3 4 5 6 7 8 9 10 11 12 13
| |--------------------------------------------------------------------|--------------------| | 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,因为这是偏向锁的特性,延迟加载
代码
1 2 3 4 5 6 7 8 9 10 11
| 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包进行的扩展方法,详细可以看这篇文章
测试禁用偏向锁
在运行配置中设置VM Options
-XX:-UseBiasedLocking
打印结果
- 因禁用了偏向锁,所以第一次打印的最后三位为
001
处于无锁的状态 - 第二次是加锁,最后三位为
000
偏向锁被禁用了,只能升级为轻量级锁,前面54为依然为ThreadId
- 第三次解锁之后,回到无锁状态
001
测试HashCode
运行配置改成取消延迟加载
1 2 3 4 5 6 7 8 9 10 11 12
| 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值