很多人一聊到 JVM 锁,第一反应就是背面试题:偏向锁、轻量级锁、重量级锁,外加一个自旋锁。说实话,这些概念单拎出来都不难懂,但真正难的是把它们串成一条线,搞清楚 JVM 到底在什么场景下选择哪种锁,以及这背后的底层逻辑到底是什么。尤其是当你遇到线上 CPU 飙高、线程大面积 BLOCKED、或者明明加了锁却没起到作用的时候,光会背概念完全不够用。
这篇就围绕 JVM 锁展开,从设计思路到对象头里的具体实现,再到用代码实测锁升级的完整链路,最后整理一份可以拿去排查线上问题的实战手册。不管你是刚接触并发编程的 Java 开发,还是准备面试想彻底吃透锁机制,或者正在为一次诡异的性能抖动头疼,这篇都会给你一个能直接落地的参考。内容偏底层但不会堆术语,我会尽量把每个细节都说明白。
1. 为什么说 JVM 锁是一套"自适应"的并发防线
JVM 锁不是一个孤立的语法特性,它本质上是在帮你解决多线程访问共享资源时的三大问题:原子性、可见性和有序性。你在代码里写一个 synchronized 或者加一个 ReentrantLock,就是在告诉 JVM:这块临界区同一时刻只能有一个线程进来,并且进出临界区时要保证共享变量的状态对其他线程可见,同时禁止编译器或 CPU 对指令进行可能破坏语义的重排序。
1.1 锁在并发编程里的位置,以及它管不住什么
我们先理清一个边界:JVM 锁管的是单个 JVM 进程内部的线程互斥。你部署了十个服务实例,每个实例都有自己的 JVM,那 synchronized 和 ReentrantLock 只能保证某一个实例内部的线程安全,跨实例的共享资源它管不了。这也是为什么后来有了分布式锁,比如基于 Redis 的实现,本质上是用一个外部协调者来让多个进程之间的线程排队。
很多人在这一步就混淆了,面试时也经常被问到"synchronized 能解决分布式并发问题吗",答案是显然不能。所以你在设计一个高并发系统的时候,第一步就要想清楚:锁的边界到底在哪里?是单机进程内,还是跨进程?这个判断直接决定了你后续用 JVM 锁还是引入分布式锁,也决定了系统的复杂度。
1.2 JVM 锁为什么要"分级",而不是一把锁走天下
如果你看过早期 JDK 的实现就会发现,synchronized 在 JDK 1.6 之前是一个不折不扣的重量级操作。它底层依赖操作系统的 Mutex Lock,线程一旦拿不到锁就会进入阻塞状态,然后被挂起,等锁释放后再被唤醒。这个阻塞和唤醒的过程涉及用户态和内核态的切换,开销非常大。在锁竞争不激烈的场景下,这种设计显得很笨重。
所以 HotSpot 团队做了一个很聪明的决定:按照锁竞争的激烈程度,把 synchronized 的实现拆成了几个级别。就像你平时出门,如果只是下楼取个快递,没必要开卡车;但如果要搬家,就得叫货车。JVM 也是同理,没有竞争的时候我就用最轻量的方式,有竞争了再逐步升级到更重的实现。这条链路就是:偏向锁 → 轻量级锁 → 重量级锁,此外还有一个很重要的自旋机制,在轻量级锁阶段配合使用。
这个设计思想值得你记住,因为它直接体现了 JVM 的核心调优思路:自适应,而不是一刀切。后面所有的参数调整、问题排查,都是在顺应这个设计的基础上做的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 锁升级实战:从偏向锁到重量级锁的完整链路
要真正理解 JVM 锁,光知道有哪几种锁是不够的,你得知道锁状态是怎么存在对象里的,又是怎么一个状态一个状态切换过去的。这一节我们深入到对象头和 Mark Word,再把整条升级链路走一遍。
2.1 对象头里的秘密:Mark Word 如何记录锁状态
HotSpot 虚拟机中,每个 Java 对象在内存里都有一块对象头(Object Header),它由两部分组成:Mark Word 和 Klass Pointer。对于普通对象来说,Mark Word 占用 8 个字节(64 位 JVM 下),里面存储的信息非常丰富,包括对象的哈希码(hashCode)、分代年龄(age)、锁状态标志位,以及持有锁的线程 ID 等等。
关键点在于:Mark Word 是一块动态复用的内存区域。它会根据对象当前的锁状态,重新解释这 8 个字节的含义。比如无锁状态下,Mark Word 里存的是哈希码和分代年龄;一旦进入偏向锁模式,同一个位置的 bit 就会被重写成线程 ID 和偏向时间戳。这也是为什么 HashCode 和偏向锁不能共存,一个对象如果已经计算过 identityHashCode,它就没法再进入偏向锁状态,因为马克字里没有多余的空间同时存下这两样东西。
不同锁状态下的 Mark Word 布局大致是这样(这里以 64 位 JVM 为例):
| 锁状态 | Mark Word 关键位 | 锁标志位 |
|---|---|---|
| 无锁 | hashCode、分代年龄 | 01 |
| 偏向锁 | 线程ID、epoch、分代年龄 | 01(且偏向位为1) |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向管程 Monitor 的指针 | 10 |
你可以看到,无锁和偏向锁的标志位都是 01,区别在于偏向锁里多了一个偏向位(biased_lock)。JVM 在读 Mark Word 的时候,会先看偏向位是否为 1,再判断具体是无锁还是偏向锁。这个细节我见过不少文章写错,这里单独拿出来提醒一下。
2.2 一步步拆解 synchronized 的升级过程
第一阶段:偏向锁
当一个线程第一次访问某个同步块时,如果 JVM 开启了偏向锁(默认开启,但有延迟),并且这个对象还没有被任何线程偏向,那么 JVM 会通过一次 CAS 操作,把当前线程的 ID 写入 Mark Word 的偏向线程 ID 字段。这个操作几乎没有任何开销,因为它只是修改线程私有的对象头,不涉及操作系统级的锁操作。
在偏向锁生效期间,这个线程每次进入同步块,只需要判断一下 Mark Word 里的偏向线程 ID
