导读:本期聚焦于小伙伴创作的《如何利用对象头的标记位实战理解 synchronized 对变量锁定的底层机制》,敬请观看详情。为什么同一段 synchronized 代码在加锁前后,对象的内存布局会发生肉眼可见的变化?这背后正是对象头中 mark word 的标记位在起作用。本文从 HotSpot 虚拟机对象头结构切入,说明 mark word 如何在无锁、偏向锁、轻量级锁和重量级锁状态下记录不同标记位。借助 OpenJDK 的 JOL 工具,我们可以打印对象头二进制,观察锁升级时标记位翻转的实际过程。理解这些底层机制,能帮助开发者判断 synchronized 是否真对目标变量生效,也能解释为何某些场景下加锁后性能反而下降。

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

如何利用对象头的标记位实战理解 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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。