Monitor 概念回顾

Java对象头

以 32 位虚拟机为例

名词解释:

  1. Object Header (64 bits): 它包含了对象的元信息以及用于垃圾回收和同步的数据。

  2. Mark Word (32 bits): 这个部分通常包含用于垃圾回收和同步的标记信息。标记字包含了对象的哈希码锁定状态垃圾回收标记等信息。

  3. 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加锁过程

  1. 无锁状态(Unlocked):一开始,对象处于无锁状态。这意味着没有线程持有该对象的锁。

  2. 偏向锁检测(Biased Lock Check):在进入同步块之前,JVM 会尝试检测对象是否已经偏向某个线程。偏向锁是一种优化,旨在减少不必要的竞争。如果对象已经被偏向某个线程,且当前线程是偏向线程,那么它可以直接进入同步块,跳过后续步骤。

  3. 自旋锁(Spin Locking):如果对象不是偏向任何线程,或者当前线程不是偏向线程,JVM 会尝试使用自旋锁来避免进入重量级锁。自旋锁是一种快速的锁获取尝试,线程会短暂自旋等待锁的释放,而不进入阻塞状态。

  4. 轻量级锁尝试(Lightweight Lock Attempt):如果自旋锁不成功,当前线程将尝试使用轻量级锁。此时,JVM会尝试在对象头中的 Mark Word 中设置标志来表示当前线程持有该对象的锁。

  5. 竞争(Contention):如果轻量级锁尝试失败,表示可能有其他线程也在竞争同一个锁,进入竞争状态。这时,JVM 将使用适当的机制来处理竞争,通常会升级锁为重量级锁。

  6. 重量级锁(Heavyweight Lock):如果竞争仍然存在,JVM 将升级锁为重量级锁。重量级锁使用操作系统的原生同步机制,例如互斥量,来确保同一时刻只有一个线程可以进入同步块。其他线程将被阻塞,直到持有锁的线程释放它。

  7. 执行同步块(Executing Synchronized Block):一旦线程成功获取锁,它可以进入同步块内执行相应的代码。只有一个线程可以同时执行同步块内的代码。

  8. 释放锁(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 的值存 入锁记录

image-20231031115626931

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

    image-20231031143349476

  • 如果 cas 失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image-20231031143607867

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一

    image-20231031143649632

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象 头

    • 成功,则解锁成功

    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image-20231031143825427

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

    • 然后自己进入 Monitor 的 EntryList BLOCKED

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况

image-20231031144036930

自旋重试失败的情况

image-20231031144119594

  • 自旋会占用 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
    }
}

image-20231031144326659

image-20231031144334098

偏向状态

|--------------------------------------------------------------------|--------------------|
|                        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,因为这是偏向锁的特性,延迟加载

image-20231101101002777

代码

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 仍存储于对象头中

image-20231101103202099

上述的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

image-20231101103711769

打印结果

  • 因禁用了偏向锁,所以第一次打印的最后三位为001处于无锁的状态

  • 第二次是加锁,最后三位为000偏向锁被禁用了,只能升级为轻量级锁,前面54为依然为ThreadId

  • 第三次解锁之后,回到无锁状态001

image-20231101103746789

测试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值