在 Java 中,synchronized 关键字是最基础的同步手段,但它究竟如何确定一个变量被锁定,很多开发者只停留在“加锁释放锁”的表层认知。实际上,JVM 并没有为每一个 synchronized 块单独维护一张锁表,而是把锁状态直接编码在对象头里的 mark word 标记位中。本文通过对象头结构和实战工具,拆解 synchronized 锁定变量的底层机制。

一、对象头与 mark word 的基础结构
在 HotSpot 虚拟机中,任何一个 Java 对象在堆内存中都由对象头、实例数据和对齐填充三部分组成。对象头又分为两部分:第一部分是与对象自身运行数据相关的 mark word,第二部分是类型指针 klass pointer。我们讨论 synchronized 锁定机制,核心就在 mark word。
mark word 的长度在 32 位虚拟机中是 32 bit,在 64 位虚拟机中是 64 bit。它是一块可变结构的内存,根据对象当前状态存放不同信息。比如无锁状态下存放对象哈希码、分代年龄;偏向锁状态下存放偏向线程 ID;轻量级锁状态下存放指向栈中锁记录的指针;重量级锁状态下存放指向操作系统互斥量(monitor)的指针。这些不同含义的编码,正是通过 mark word 里的若干标记位来区分的。
以 64 位虚拟机为例,mark word 最后两位是锁标记位(lock bits),它们和前面的比特组合,共同表达对象处于哪种锁状态。例如最后两位为 01 且偏向标志位为 0 表示无锁,为 1 表示可偏向;00 表示轻量级锁;10 表示重量级锁;11 表示 GC 标记。理解这些标记位,是看懂 synchronized 底层行为的前提。
二、synchronized 如何借助标记位锁定变量
当我们用 synchronized 修饰一个对象变量,例如对某个实例对象加锁,JVM 在字节码层面会插入 monitorenter 和 monitorexit 指令。执行 monitorenter 时,虚拟机首先读取目标对象的 mark word,根据标记位判断当前锁状态,再决定是偏向、膨胀还是直接竞争。
如果对象处于无锁状态且开启了偏向锁,JVM 会用 CAS 操作把 mark word 中的部分比特修改为当前线程 ID,并把偏向标志位置为 1,此时对象就被“逻辑锁定”在该线程手中,后续该线程进入同步块无需额外原子操作。如果另一线程尝试进入,发现偏向标志指向别人,就会触发偏向撤销和锁膨胀,mark word 标记位变为 00 或 10,进入轻量级或重量级锁逻辑。
值得强调的是,synchronized 锁定的是对象而非变量名。也就是说,如果你写 synchronized(obj) 或者 synchronized 实例方法,锁信息永远写在 obj 这个对象头的 mark word 里。如果多个变量引用同一个对象,它们加锁实际竞争的是同一份标记位;反之不同对象就有不同 mark word,彼此互不干扰。
三、实战:用 JOL 观察标记位变化
OpenJDK 提供的 JOL(Java Object Layout)工具可以直接打印对象内存布局,让我们在不读源码的情况下,肉眼看到 mark word 的标记位翻转。下面是一段演示代码:
import org.openjdk.jol.info.ClassLayout;
public class SynchronizedMarkWordDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
// 打印无锁状态下的对象头
System.out.println("无锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
synchronized (lock) {
// 打印持有锁时的对象头
System.out.println("轻量级或重量级锁状态:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
// 再次打印释放锁后的对象头
System.out.println("释放锁后:");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
运行上面的程序,你会看到对象头第一行就是 mark word 的十六进制与二进制。无锁时末尾通常是 01,进入 synchronized 块后,根据 JVM 版本和竞争情况,末尾可能变成 00(轻量级锁,栈指针写入 mark word)或 10(重量级锁,指向 monitor)。退出同步块后,若未发生严重竞争,对象可能恢复为无锁或保留偏向信息。
通过这种实战打印,我们可以确认 synchronized 是否真的对目标变量生效。例如有人误以为 synchronized 锁住方法里的局部变量就能同步线程,其实局部变量每次新建对象,mark word 互不相同,根本无法形成互斥。只有让多线程竞争同一个对象的 mark word 标记位,锁才有意义。
四、从标记位看锁升级与性能影响
标记位不仅表示“有没有锁”,还记录了锁的级别。从偏向锁到轻量级锁再到重量级锁,本质是 mark word 中标记位与关联指针的切换过程。偏向锁几乎零开销,但遇到多线程竞争就要撤销;轻量级锁用 CAS 和自旋避免内核态切换;重量级锁则依赖操作系统的 mutex,线程阻塞带来明显开销。
在实战调优时,如果你通过 JOL 或加上 -XX:+PrintAssembly 观察到大量对象 mark word 在 10 重量级状态,说明 synchronized 竞争剧烈。此时单纯靠理解标记位已不够,需要考虑减小同步块、使用并发容器或改用 ReentrantLock 等策略。但无论如何,底层出发点都是:锁状态写在对象头标记位里,变量锁定只是该对象被标记的结果。
总结来说,利用对象头的 mark word 标记位,我们能把 synchronized 从黑盒变成可观测的实体。掌握无锁、偏向、轻量、重量四种状态对应的比特布局,再用 JOL 做几次打印实验,就能真正理解所谓“变量锁定”在 JVM 中到底发生了什么。
synchronized对象头mark_word修改时间:2026-08-06 08:33:30