Appearance
JUC synchronized锁升级
本文主要介绍synchronized锁升级相关知识,包括Monitor、偏向锁、轻量级锁和重量级锁。
1. 对象头
1.1 对象头概述
在Java中,每个对象不仅包括实例数据,还包括对象头,其中存储着一些关于对象的信息。
以32位虚拟机为例,普通对象对象头如下,包含Mark Word和Klass Word:
txt
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|数组对象的对象头如下,除了Mark Word和Klass Word,还包括array length:
txt
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|1.2 对象头的组成
1.2.1 Mark Word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。 为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
txt
|----------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|----------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1(0) | lock:2 | Normal |
|----------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1(1) | lock:2 | Biased |
|----------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|----------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|----------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|----------------------------------------------------------|--------------------|其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
| biased_lock | lock | 状态 |
|---|---|---|
| 0 | 01 | 无锁(正常) |
| 1 | 01 | 偏向锁 |
| 0 | 00 | 轻量级锁 |
| 0 | 10 | 重量级锁 |
| 0 | 11 | GC标记 |
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
64位下的标记字与32位的相似,不再赘述。
1.2.2 Klass Word
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM长度为32位,64位的JVM长度为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer,即普通对象指针。开启该选项后,下列指针将压缩至32位:
- 每个
Class的属性指针(即静态变量) - 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
1.2.3 array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
2. Monitor对象
2.1 Monitor原理
Monitor,又称为监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级锁)之后,该对象的Mark Word中就有指向Monitor对象的指针。
现有多个线程对同一个对象加锁,即synchronized(obj):
- 当有线程(Thread-2)给对象加锁时,该对象的
Mark Word(此时对象头状态为10,重量级锁)指向Monitor对象; - 把Monitor中的
Owner指向该线程(Thread-2); - 如果有其他线程(Thread-3、Thread-4、Thread-5)试图给对象加锁,则会被阻塞,进入
EntryList中等待; - 如果获得锁的线程(Thread-2)已执行完,则放弃锁,唤醒被阻塞(
BLOCKED)的线程来竞争锁; WaitSet中的线程(Thread-0、Thread-1)是之前获得过锁,但条件不满足放弃锁,进入WAITING/TIMED WAITING状态的线程。
注意:
- 如果只是普通对象,即不是锁对象,则不会关联Monitor对象,不遵守上述规则;
- 只有多线程竞争同一把锁时才会出现上述情况;

2.2 Monitor字节码分析
现有如下代码:
java
static final Object lock = new Object();
private static int count = 0;
public static void main(String[] args) {
synchronized (lock){
count++;
}
}使用javap -p -v xxx命令反编译字节码会产生如下指令:
txt
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field count:I
9: iconst_1
10: iadd
11: putstatic #3 // Field count:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any可以看到,在Java代码中写的synchronized关键字,会在字节码层面生成monitorenter和monitorexit指令:
- 在第5行指令执行了
monitorenter指令,该指令会使线程尝试获取锁对象(lock对象)关联的Monitor对象,即将Monitor对象的Owner字段指向当前线程,如果获取失败,则会导致当前线程进入阻塞状态,即进入Monitor对象的EntryList字段中; - 在第15行指令执行了
monitorexit指令,该指令会使线程正常退出同步代码块,即当前线程会释放Monitor对象的所有权,并且唤醒阻塞队列中的线程,让他们竞争获取锁; - 在第21行指令同样执行了
monitorexit指令,该指令是为了异常发生导致退出时,线程同样也可以释放锁。在下面的异常表中,我们可以看到有两条记录:第一条记录是指如果第6行到第16行指令发生异常,则跳转到第19行指令处理异常(第19行就是异常处理指令,保证释放锁);第二条记录是指如果第19行到第22行指令发生异常,则跳转到第19行指令处理异常;
综上,当我们使用synchronized时,Java为我们添加了monitorenter和monitorexit指令,使得我们可以正常获取锁和释放锁,同时,也自动添加了异常处理机制,使得在异常发生时,也可以释放锁。
除了使用monitorenter和monitorexit指令,Java编译器还为我们添加了临时的局部变量槽,在下面的信息中,locals=3表示局部变量表有三个槽位:
txt
stack=2, locals=3, args_size=1但是在代码中只明确声明了一个参数 args,并没有其他局部变量,这看起来有点不合理。
原因在于 Java 编译器在处理 synchronized 语句时,会生成额外的字节码,这些字节码需要使用局部变量槽来存储临时信息。
在给定的 main 方法代码中,局部变量的最大槽数是 3。这可能初看起来有点反直观,因为代码中只明确声明了一个参数 args,并没有其他局部变量。
原因在于 Java 编译器在处理 synchronized 语句时,会生成额外的字节码,这些字节码需要使用局部变量槽来存储临时信息。
每个槽位存储的内容具体来说如下:
String[] args参数: 这是main方法的参数,它占用一个局部变量槽(通常是槽位 0)。synchronized (lock)块:synchronized语句需要获取并最终释放给定对象的监视器(monitor)。为了正确处理正常退出和异常退出(确保在异常发生时也能释放锁),编译器会生成包含monitorenter和monitorexit指令的字节码。在这个过程中,需要一个临时局部变量槽来存储被锁定的对象引用 (lock),以便在monitorexit指令中使用,尤其是在异常处理路径中。这个临时变量通常占用一个槽(槽1,如上面的astore_1指令)。- 异常处理的临时变量:
synchronized块的实现隐含地包含了一个try-finally结构,或者更准确地说,一个通过异常表实现的等价结构。当 synchronized 块内的代码抛出异常时,控制流会跳转到异常处理代码,这段代码会执行monitorexit来释放锁,然后重新抛出捕获到的异常。为了临时持有这个被捕获的异常对象以便重新抛出,编译器会使用另一个临时局部变量槽(槽2,如上面的astore_2指令)。
因此,尽管在源代码中只看到了 args,但编译器为了实现 synchronized 块的语义(特别是其健壮的异常处理),会使用额外的两个局部变量槽来存储临时的对象引用和异常对象。
总结起来,这 3 个槽分别是:
- 槽 0:
args参数 - 槽 1: 存储
lock对象引用的临时变量(由synchronized生成) - 槽 2: 存储捕获到的异常对象的临时变量(由
synchronized的异常处理生成)
3. synchronized锁升级
3.1 重量级锁
重量级锁,就是利用Monitor对象来决定锁对象的拥有者,此时对象头的状态为10。
3.2 轻量级锁
3.2.1 概念
在 Java 的 HotSpot JVM 中(JDK 1.6),轻量级锁(Lightweight Locking)是 synchronized 关键字实现的一种锁优化技术。它是为了在没有实际锁竞争的情况下,减少传统重量级锁(使用操作系统互斥量)带来的性能开销而引入的。
轻量级锁假设大多数锁在整个同步周期内都没有竞争,即线程1持有锁对象期间,没有其他线程竞争锁对象。它试图在用户空间(User Space)而非内核空间(Kernel Space)完成同步操作,从而避免操作系统线程上下文切换的昂贵开销。
轻量级锁主要依赖于两个概念:
- 锁记录(Lock Record): 每个线程在自己的栈帧中会分配一块区域,用作锁记录。当线程尝试获取轻量级锁时,就会在自己的栈上创建这样一个锁记录。
- 对象头的 Mark Word: 每个对象都有一个对象头,其中的 Mark Word 用于存储对象的运行时数据,包括锁状态信息。
轻量级锁对使用者是透明的,即我们仍然使用synchronized关键字。
3.2.2 锁的获取
假设有如下代码:
java
static final Object obj = new Object();
public static void method1(){
synchronized (obj){
// 同步代码块
}
}当线程0执行method1()方法时,会获取轻量级锁,过程如下:
创建锁记录: 当线程0尝试获取一个对象 obj 的锁时,如果需要创建轻量级锁,JVM 会在线程0的栈帧中创建一个锁记录。

复制对象头: JVM 会将对象 obj 当前的 Mark Word 复制到线程0的锁记录的
displaced_header字段中。
CAS 尝试修改对象头: 线程 0 使用 CAS (Compare-and-Swap) 操作尝试将对象 obj 的 Mark Word 替换为指向线程 0 栈中这个锁记录的指针。
如果 CAS 成功: 线程 0 成功获取轻量级锁。此时,对象 obj 的 Mark Word 指向线程 0 的栈中的锁记录,锁状态变为轻量级锁。

如果 CAS 失败: 这通常意味着发生了锁竞争(有其他线程也在尝试获取同一个锁)。轻量级锁就会**膨胀(升级)**为重量级锁。
3.2.3 锁的释放
当持有轻量级锁的线程退出同步块时,它会尝试使用 CAS 操作将对象 obj 的 Mark Word 恢复为锁记录(Lock Record)中保存的原始 Mark Word 值。
- 如果 CAS 成功: 说明在持有锁期间没有竞争发生,锁被成功释放,对象头恢复到之前的状态(如无锁)。
- 如果 CAS 失败: 说明在持有锁期间发生了竞争,锁已经膨胀为重量级锁。线程会按照重量级锁的方式来释放锁。
3.2.4 锁膨胀
如果某个线程(Thread-1)在为对象加轻量级锁的过程中,CAS操作无法成功,有一种情况是有其他线程已经为该对象加上了轻量级锁,这是需要进行锁膨胀,将轻量级锁变为重量级锁。
当Thread-1进行轻量级加锁时,Thread-0已经为该对象加上了轻量级锁:

这是Thread-1加轻量级锁失败,进入锁膨胀流程:
- 为obj对象申请Monitor锁,让obj对象头状态变为重量级锁(10),并且指向Monitor对象;
- 自己(Thread-1)进入Monitor的
EntryList队列进行阻塞;

当Thread-0退出同步代码块解锁时,使用CAS将Mark Word的值恢复给对象头,但是操作失败,这时会进入重量级解锁流程,即按照对象头中的Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的阻塞线程;
3.3 偏向锁
3.3.1 概念
偏向锁(Biased Locking)是 Java HotSpot JVM 中 synchronized 关键字的一种最乐观的锁优化技术。
它的核心思想是,在大多数情况下,锁不仅没有多线程竞争,而且总是由同一个线程多次获得。
基于这种假设,偏向锁的目标是消除在无竞争情况下的同步开销,即使是轻量级锁所需的 CAS 操作。
DANGER
但是,在Java 15中,由于维护成本过高,偏向锁已经被标记为废弃状态,且默认不开启,详情参考:https://openjdk.org/jeps/374。
所以对于偏向锁的知识,我们了解即可。
3.3.2 程序准备
以下代码示例均在JDK 1.8下演示。
首先准备jol依赖库:
xml
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>该库可显示对象内存的内存布局。
使用案例:
java
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}结果如下:
txt
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total可以看到输出了obj对象的mark word内容和klass word内容。
在JDK 1.8 下,默认开启了偏向锁,即对象创建后,对象头中的mark word最后三位为101(十六进制下为0x05),但是thread、epoch和 age都是0,但是生效是有延迟的,不会在程序启动时立即生效,如果想避免延迟,可以使用虚拟机参数-XX:BiasedLockingStartupDelay=0来禁用延迟。
添加虚拟机参数禁用偏向锁延迟后,输出结果如下:
txt
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total可以看到mark word中偏向锁标志位已经为1 了,表示已生效。
3.3.3 偏向锁示例
编写如下代码,创建一个对象obj,并且在加锁前、加锁中和解锁后打印对象内存布局:
java
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("加锁前...");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁...");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后...");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}详细结果如下:
Details
txt
加锁前...
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁...
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后...
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total我们只需要查看Mark Word内容,整理如下:
txt
加锁前:0x0000000000000005 (biasable; age: 0)
加锁中:0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)
解锁后:0x000000014700d805 (biased: 0x000000000051c036; epoch: 0; age: 0)可以看到obj对象的mark word在加锁后,thread 段有了内容(0x000000000051c036,即主线程ID),并且在解锁后,mark word内容也没恢复到和加锁前一样,说明obj对象偏向于主线程。
3.3.4 偏向锁的撤销
1. 有其他线程加锁
当某个锁对象已经偏向线程A后,如果线程B再对该对象加锁,则会导致该对象锁升级为轻量级锁。此处又分为两种情况:
- 情况一:线程B加锁时,线程A已不再使用锁,则线程B加锁时会升级为轻量级锁;
- 情况二:线程B加锁时,线程A还在使用锁,则会导致轻量级锁升级为重量级锁;
情况一示例:
Details
java
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
Thread.sleep(1000); // 睡眠1秒,保证线程B后执行
new Thread(()->{
System.out.println("线程B加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("线程B加锁中:"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("线程B解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
},"B").start();
Thread.sleep(1000); // 睡眠1秒,保证主线程在线程B运行完后执行
System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
}完整结果如下:
Details
txt
加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程B加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000010f808805 (biased: 0x000000000043e022; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程B加锁中:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00000001729f2950 (thin lock: 0x00000001729f2950)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
线程B解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000016fd72a10 (thin lock: 0x000000016fd72a10)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total经过分析,在线程A(代码示例中的主线程)加锁时,是轻量级锁;当线程B加锁时,此时线程A已释放了锁,所以线程B加的是轻量级锁;在线程B之后,线程A再加锁,仍然是轻量级锁。
情况二示例:
Details
java
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(()->{
System.out.println(LocalDateTime.now() + " A加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
try {
System.out.println(LocalDateTime.now() + " A加锁时1:"+ClassLayout.parseInstance(obj).toPrintable());
Thread.sleep(2000);
System.out.println(LocalDateTime.now() + " A加锁时2:"+ClassLayout.parseInstance(obj).toPrintable());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(LocalDateTime.now() + " A解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
},"A").start();
Thread.sleep(1000);
new Thread(()->{
System.out.println(LocalDateTime.now() + " B加锁前:"+ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println(LocalDateTime.now() + " B加锁时:"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(LocalDateTime.now() + " B加锁后:"+ClassLayout.parseInstance(obj).toPrintable());
},"B").start();
}完整结果如下:
Details
txt
2025-05-03T10:01:33.821 A加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:34.274 A加锁时1:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012a064805 (biased: 0x00000000004a8192; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:34.813 B加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012a064805 (biased: 0x00000000004a8192; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:36.279 A加锁时2:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000011980d492 (fat lock: 0x000000011980d492)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:36.281 A解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000011980d492 (fat lock: 0x000000011980d492)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:36.281 B加锁时:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000011980d492 (fat lock: 0x000000011980d492)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
2025-05-03T10:01:36.283 B加锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000011980d492 (fat lock: 0x000000011980d492)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total可以发现,如果在线程A持有锁的期间,线程B对同一个锁对象加锁,那么偏向锁会升级为重量级锁。
2. 调用了hashCode()
在无锁状态下,hashCode存储在对象头中,偏向锁也存储在对象头中,所以两者冲突,必然导致偏向锁升级。此处又分为两种情况讨论:
- 情况一:在加锁前调用
hashCode()方法,那么会导致偏向锁升级为轻量级锁。 - 情况二:在加锁过程中调用
hashCode()方法,那么会导致偏向锁升级为重量级锁。
情况一代码示例:
Details
java
public static void main(String[] args) {
Object obj = new Object();
System.out.println("加锁前 hashCode前:" + ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
System.out.println("加锁前 hashCode后:" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁中:" + ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
}结果如下:
txt
加锁前 hashCode前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁前 hashCode后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000004b53f53801 (hash: 0x4b53f538; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000016e04aa20 (thin lock: 0x000000016e04aa20)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000004b53f53801 (hash: 0x4b53f538; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total可以看到,在加锁前调用hashCode()方法,会导致直接加轻量级锁,而不是偏向锁。
情况二代码示例:
Details
java
public static void main(String[] args) {
Object obj = new Object();
System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁中 hashCode前:" + ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
System.out.println("加锁中 hashCode后:" + ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后:"+ClassLayout.parseInstance(obj).toPrintable());
}结果如下:
txt
加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中 hashCode前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000139009805 (biased: 0x00000000004e4026; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中 hashCode后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012880be92 (fat lock: 0x000000012880be92)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012880be92 (fat lock: 0x000000012880be92)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total在加锁过程中调用锁对象的hashCode()方法,会导致偏向锁直接升级为重量级锁。
3. 调用wait()或notify()方法
关于wait()和notify()的用法,会在下一章着重讲解。
由于wait()和notify()是和Monitor对象关联的,所以调用了wait()和notify()方法后,会导致偏向锁升级为重量级锁。
代码示例如下:
Details
java
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
new Thread(()->{
System.out.println("加锁前:" + ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj){
System.out.println("加锁中 wait前:" + ClassLayout.parseInstance(obj).toPrintable());
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("加锁中 wait后:" + ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println("解锁后:" + ClassLayout.parseInstance(obj).toPrintable());
},"A").start();
Thread.sleep(1000); // 确保在A线程之后执行
synchronized (obj){
obj.notify();
}
}结果如下:
txt
加锁前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中 wait前:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012a8e7005 (biased: 0x00000000004aa39c; epoch: 0; age: 0)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
加锁中 wait后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012c01bc92 (fat lock: 0x000000012c01bc92)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
解锁后:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000012c01bc92 (fat lock: 0x000000012c01bc92)
8 4 (object header: class) 0x00000f28
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total3.3.5 批量重偏向和消除
批量重偏向是指当同一类的很多对象都偏向于同一个线程 t1,但这些对象现在频繁(超过某个阈值)被另一个线程 t2 尝试获取锁,导致频繁发生偏向锁撤销,由于偏向锁撤销是一个相对重量级的操作,通常需要在安全点(Safepoint)进行,所以JVM 会修改该类的元数据,当超过阈值后线程t2尝试获取锁时,JVM 不会再尝试撤销对线程 t1 的旧偏向。相反,它会尝试将对象直接偏向于当前正在获取锁的线程(线程 t2)。
批量消除是指某一个类的很多对象频繁发生批量重偏向,JVM会直接将该类对象的偏向锁功能锁定,使得该类的现有对象直接进入轻量级锁状态,新对象直接进入无锁状态。
代码示例:现有线程t1对Object类的多个对象加锁,此时是偏向锁,锁对象偏向于t1线程。t1线程运行结束后,t2线程对这些Object对象加锁,此时会先发生锁撤销(即升级为轻量级锁),但是超过一定阈值后,会将剩余对象的锁状态直接偏向t2线程。t2线程运行结束后,t3线程仍然对这些Object对象加锁,此时仍然会先发生锁撤销(升级为轻量级锁),但是由于该类对象撤销次数过多(超过另一个更高的阈值),JVM会将该类对象设置为不可偏向,则最终这些对象均为轻量级锁。最后,主线程新建一个Object对象,此时发现新建的对象是无锁状态,即禁用了该类对象的偏向锁。
注意:如果一个对象是无锁状态(001),那么对该对象加锁,不会加轻量级锁,而是会从轻量级锁开始。
注意:下面代码使用了附录中的工具类MarkWordUtil
Details
java
public static void main(String[] args) throws InterruptedException {
Thread t1,t2,t3;
Vector<Object> vector = new Vector<>();
final int loopNumber = 40;
t1 = new Thread(()->{
for (int i = 0; i < loopNumber; i++) {
Object obj = new Object();
vector.add(obj);
synchronized (obj){
MarkWordUtil.printMarkWord(obj);
}
}
synchronized (vector){
vector.notify();
}
},"t1");
t1.start();
t2 = new Thread(()->{
synchronized (vector){
try {
vector.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("==================");
for (int i = 0; i < loopNumber; i++) {
synchronized (vector.get(i)){
MarkWordUtil.printMarkWord(vector.get(i));
}
}
synchronized (Vector.class){
Vector.class.notify();
}
},"t2");
t2.start();
t3 = new Thread(()->{
synchronized (Vector.class){
try {
Vector.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("==================");
for (int i = 0; i < loopNumber; i++) {
synchronized (vector.get(i)){
MarkWordUtil.printMarkWord(vector.get(i));
}
}
}
},"t3");
t3.start();
t3.join();
System.out.println("==================");
MarkWordUtil.printMarkWord(new Object());
}结果:
txt
2025-05-03T11:39:56.878 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.878 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.879 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.879 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.880 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.881 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.882 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.883 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.884 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.885 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.886 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.886 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.887 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.888 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
2025-05-03T11:39:56.888 [t1] 00000000 00000000 00000000 00000001 00011111 00000101 10011000 00000101
==================
2025-05-03T11:39:56.888 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.889 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.890 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 01110000 00100011 10101001 01000000
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.891 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.892 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.893 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.894 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.895 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.896 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
2025-05-03T11:39:56.897 [t2] 00000000 00000000 00000000 00000001 00011111 10010100 00001001 00000101
==================
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.897 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.898 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.899 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.900 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.901 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
2025-05-03T11:39:56.902 [t3] 00000000 00000000 00000000 00000001 01110000 01000100 01101001 00110000
==================
2025-05-03T11:39:56.902 [main] 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000013.4 小结
使用synchronized对对象加锁,从偏向锁-->轻量级锁-->重量级锁,效率逐渐降低。
4. 锁重入
所谓锁重入,是指某个线程t1对某个对象A加锁,在外层同步代码块内,还可以对该对象加锁,用代码表示如下:
java
public static void main(String[] args) {
Object obj = new Object();
synchronized (obj){
synchronized (obj){
System.out.println("hello");
}
}
}"hello"正常输出,说明synchronized的锁是可以重入的。
4.1 重量级锁的锁重入
重量级锁的锁重入是通过Monitor对象的Count来实现的,Count 字段是一个计数器,用来记录当前持有 Monitor 的线程重入(reenter)这个锁的次数。
当线程A第一次持有锁时:会将锁对象的Monitor对象中的Owner字段指向当前线程(即线程A),并将 _count 字段设置为 1。
当线程A在持有锁对象的时候,再次获取同一对象的锁时,并不会导致线程A的阻塞,而是会把Count字段的值加1。
当线程A释放锁时,会将所对象的Monitor对象中的Count字段的值减1:
如果
Count递减后仍然大于 0,说明线程 A 还有其他层级的重入锁没有释放。线程 A 仍然持有锁,Owner字段保持不变。如果
Count递减后等于 0,说明线程 A 已经完全退出了这个锁的最外层同步块。线程 A 不再持有锁。JVM 会将Owner字段设置为空,然后检查是否有其他线程在等待队列 (EntryList) 中,如果有,就会唤醒等待的线程来竞争锁。
简单来说,重量级锁的重入机制就是依靠 Monitor 内部的一个计数器。持有锁的线程每次“重新进入”同步块时,计数器加一;每次退出同步块时,计数器减一。只有当计数器归零时,锁才会被真正释放,其他等待的线程才有机会获取锁。
4.2 轻量级锁的锁重入
轻量级锁的锁重入是通过创建多个锁记录(Lock Record)来实现的,当某个线程多次获取同一个对象的轻量级锁时,会创建多个锁记录。
当线程A第一次持有锁时,会在线程A的栈中创建一个锁记录,并且对象头中的Mark Word指向该锁记录地址:

当线程A在持有对象的轻量级锁时,再次获取该对象的锁,JVM会在栈中创建一个新的锁记录,新的锁记录的 displaced_header 字段不会复制对象的 Mark Word,而是设置为一个特殊的标记值(通常是 null ),用来表示这是一个重入锁记录。并且不会再次尝试修改对象的 Mark Word,对象的 Mark Word 仍然指向第一次获取锁时创建的那个锁记录。

当释放锁时,通过检查锁记录的displaced header的值,就能判断该锁是不是重入的锁:
- 当线程退出一个
synchronized代码块时,JVM 会找到当前栈帧中与该对象关联的最顶层的锁记录。 - 如果锁记录的
displaced_header字段是那个特殊的重入标记 (null): 说明这是一个重入锁记录。JVM 直接将这个锁记录从栈中弹出/丢弃。对象的 Mark Word 不变。线程仍然持有锁。 - 如果锁记录的
displaced_header字段是原始的对象 Mark Word 值: 说明这是该线程为这个对象创建的**最底层的(第一次)**锁记录,按照轻量级锁的解锁步骤进行解锁。
轻量级锁的重入依赖于线程栈上的锁记录链。每次重入都在栈上增加一个标记为重入的锁记录。对象的 Mark Word 只指向最底层的锁记录。释放锁时,从栈顶开始逐个弹出锁记录,直到弹出带有原始 Mark Word 的锁记录时,才尝试通过 CAS 恢复对象头,完成锁的真正释放。
4.3 偏向锁的锁重入
偏向锁记录了线程ID,天生是可以重入的。
5. 自旋优化
当发生锁竞争的时候,可以使用自旋来进行优化。所谓自旋,就是进行几次无意义的循环,以期待在循环期间,其他线程释放了锁,这时当前线程就可以避免阻塞。
注意
只有在多CPU的机器上,自旋优化才有意义。
以下是自旋成功的图示:

6. 锁消除
锁消除(Lock Elimination)是 Java 虚拟机(JVM)的即时编译器(JIT Compiler)在运行时进行的一种优化技术。
它的核心思想是:如果 JVM 经过分析,发现一个对象虽然使用了同步机制(比如 synchronized),但实际上它不可能被多个线程共享访问,那么这个对象的锁操作就是完全不必要的,可以直接将这些锁操作消除掉。
例如:
java
public void method(){
Object obj = new Object();
synchronized (obj){
// 同步代码块
}
}在上面的方法中创建了局部变量obj,但是该局部变量没有发生逃逸,因此是线程私有的,所以对其加锁是没有意义的,所以JVM会在运行时,将锁操作消除。
附录
MarkWordUtil 工具类
MarkWordUtil 工具类只打印对象头,而不输出其他内容:
java
import org.openjdk.jol.info.ClassLayout;
import java.time.LocalDateTime;
public class MarkWordUtil {
public static void printMarkWord(Object o) {
ClassLayout classLayout = ClassLayout.parseInstance(o);
String printable = classLayout.toPrintable();
String[] split = printable.split("\n");
String substring = split[2].substring(split[2].indexOf("0x"));
System.out.println(LocalDateTime.now() + " [" +Thread.currentThread().getName() + "] " + hexToBinaryWithSpaces(substring.substring(0, 18)));
}
private static String hexToBinaryWithSpaces(String hexString) {
// 去掉可能存在的"0x"前缀
String cleanHexString = hexString.startsWith("0x") ? hexString.substring(2) : hexString;
StringBuilder binaryBuilder = new StringBuilder();
// 将每个十六进制字符转换为4位二进制
for (char hexChar : cleanHexString.toCharArray()) {
String binaryChar = Integer.toBinaryString(Character.digit(hexChar, 16));
// 确保每4位二进制数是4位长度(前面补0)
binaryBuilder.append(String.format("%4s", binaryChar).replace(' ', '0'));
}
// 插入空格使得每8个二进制字符间有一个空格
StringBuilder resultBuilder = new StringBuilder();
for (int i = 0; i < binaryBuilder.length(); i += 8) {
if (i > 0 && i % 8 == 0) {
resultBuilder.append(" ");
}
resultBuilder.append(binaryBuilder.substring(i, Math.min(i + 8, binaryBuilder.length())));
}
return resultBuilder.toString().trim(); // 移除可能的尾随空格
}
}