对象锁在虚拟机里并不是一把“锁”,而是一套从 Java 字节码一路下沉到 C++ 对象头的状态机:HotSpot 把锁状态记录在对象头的 Mark Word 中,按竞争激烈程度依次升级为偏向锁、轻量级锁、重量级锁,真正阻塞线程的那把锁由 ObjectMonitor 承担。
对象头里的“状态栏”:锁到底存在哪
很多人以为 synchronized 会给对象挂一个锁对象,其实虚拟机没有额外分配任何“锁结构”,锁的元信息就压在对象自己身上。
Mark Word:对象头里的那32位或64位
HotSpot 的对象在内存里由三部分组成:对象头、实例数据、对齐填充,对象头又拆成 Mark Word 和 Klass Pointer,Mark Word 是个“复用字段”,谁需要谁占:
- 无锁状态时,它存对象的哈希码、分代年龄、锁标志位
- 偏向锁时,它存偏向线程 ID、Epoch、分代年龄
- 轻量级锁时,它存指向线程栈中 Lock Record 的指针
- 重量级锁时,它存指向 ObjectMonitor 的指针
64 位虚拟机下 Mark Word 通常是 8 字节,最低两位是锁标志位:01 表示无锁或偏向锁,00 表示轻量级锁,10 表示重量级锁,11 表示 GC 标记,这就是虚拟机判断“这个对象现在被谁锁着”的全部依据,没有哈希表,也没有全局锁登记簿。
从字节码到 C++:monitorenter 之后发生了什么
写一段最普通的同步代码:
public void doWork() {
synchronized (this) {
count++;
}
}
javap -v 看字节码,同步块前后会插上 monitorenter 和 monitorexit,而且编译器会额外生成一条异常路径上的 monitorexit,这就是同步块里抛异常也不会死锁的原因,如果锁的是方法,字节码里没有指令,靠方法访问标志 ACC_SYNCHRONIZED 标记,JVM 调用时自行判断。
字节码解释执行时,monitorenter 最终走到 HotSpot 的 InterpreterRuntime::monitorenter,接下来的判断顺序就是锁升级的主线:先看能否偏向,再看能否轻量级 CAS,最后才膨胀成重量级,业内专家指出,理解这条调用链,比死记“锁升级”三个字有用得多。
synchronized对象锁在JVM中怎么实现的:三级膨胀全过程
偏向锁:只有一个线程时的“认主”
绝大多数同步块一辈子只被一个线程进进出出,偏向锁的思路很朴素:既然是同一个人,就别每次都验身份。
第一次加锁时,虚拟机用 CAS 把当前线程 ID 写进 Mark Word,标志位设为偏向,之后这个线程再来,只比对一下 ID 相等就直接进去,连 CAS 都省了,只有另一个线程来抢,才需要撤销偏向。
实操层面有两个坑:
- 偏向锁默认有约 4 秒的启动延迟,因为 JVM 启动阶段竞争激烈,批量撤销反而亏,调试时可以加
-XX:BiasedLockingStartupDelay=0关掉延迟 - 从 JDK 15 起偏向锁默认关闭(JEP 374),JDK 18 之后相关代码已经被移除,所以查资料时要注意版本,网上大量“偏向锁”文章对应的是 JDK 8 语境
确认当前 JDK 状态可以直接跑:
java -XX:+PrintFlagsFinal -version | grep -i biased
看到 UseBiasedLocking = false,就说明这把锁在你的环境里根本不存在。
轻量级锁:短兵相接时的 CAS
偏向失败、又还没到真正阻塞的程度,虚拟机会走轻量级锁,流程分三步:
- 在当前线程栈帧里划出一块 Lock Record,把对象原来的 Mark Word 复制进去,叫 Displaced Mark Word
- 用 CAS 尝试把对象头的 Mark Word 替换成指向这个 Lock Record 的指针
- CAS 成功就拿到锁;失败说明有竞争,先自旋重试一阵,还不行就膨胀
轻量级锁的设计前提是“锁持有时间短、几乎不冲突”,它不挂起线程,靠 CPU 空转换时间,如果你的同步块里有 IO、有远程调用、有长循环,自旋纯属浪费 CPU,这时候升级到重量级锁反而是好事。
重量级锁:ObjectMonitor 接管一切
膨胀之后,Mark Word 指向一个 C++ 结构体 ObjectMonitor,源码在 HotSpot 的 objectMonitor.hpp 里,几个关键字段值得记住:
_owner:当前持有锁的线程_recursions:重入次数,synchronized是可重入的_cxq:竞争队列,新来的线程先在这里 CAS 排队_EntryList:等待获取锁的线程集合_WaitSet:调用wait()后被挂起的线程
线程拿不到锁就进入内核态挂起,唤醒要走操作系统调度,一次上下文切换的开销,比自旋几十次都大,这就是“重量级”这个叫法的由来,它的好处是竞争再激烈也不会空耗 CPU。
| 锁状态 | Mark Word 存什么 | 竞争应对方式 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 偏向线程 ID | 直接比对,无需 CAS | 单线程反复进入 |
| 轻量级锁 | 栈上 Lock Record 指针 | CAS 加自旋 | 交替执行、冲突少 |
| 重量级锁 | ObjectMonitor 指针 | 挂起与唤醒 | 竞争激烈、持锁久 |
Java对象锁和类锁的区别是什么:别把两把锁当一把用
这是面试和线上事故里的高频混淆点,两者的底层机制一样,但锁的“载体”完全不同。
- 对象锁锁的是实例。
synchronized修饰实例方法,或者synchronized(this),锁的是当前对象实例,不同实例之间互不干扰 - 类锁锁的是
Class对象。synchronized修饰静态方法,或者synchronized(Xxx.class),全虚拟机里这个类的 Class 对象只有一个,所以是全局互斥
来看一个典型翻车场景:
public class Counter {
private static int total;
public synchronized void add() { total++; } // 锁的是实例
}
new 出三个 Counter 并发调 add(),三个线程各持各的锁,total++ 照样出问题,改成 public static synchronized void add() 或者锁 Counter.class 才有效。
行业共识认为,静态变量配实例锁是最容易埋雷的组合,因为表面上方法加了 synchronized,读代码的人很容易默认它是线程安全的,更隐蔽的是 Integer 常量池、字符串常量池做锁对象,两个毫不相干的模块可能锁到同一个对象上。
高并发场景下对象锁性能怎么优化
先测量,再优化:用 JOL 看对象头
别急着改代码,加一行 JOL 依赖,就能把 Mark Word 打出来:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
前后两段输出对比,锁标志位的变化一目了然,这比看十篇文章都直观。
锁消除与锁粗化:虚拟机替你做的两件事
- 锁消除:JIT 通过逃逸分析发现锁对象没有逃出当前线程,直接删掉同步,典型例子是
StringBuffer的append,局部变量场景下那些同步开销会被优化掉 - 锁粗化:一连串对同一个对象的加锁解锁,虚拟机会合并成一次大锁,避免反复进出
这两项是 JIT 自动完成的,但前提是你的代码别让对象随便逃逸。
减少临界区本身,比调锁参数更有效
- 把不影响共享状态的逻辑挪出同步块,临界区越短越好
- 能用
java.util.concurrent里的LongAdder、AtomicInteger就别用锁,CAS 无阻塞 - 读多写少优先
ReentrantReadWriteLock或StampedLock - 确实要长时间持锁,考虑拆锁:按 key 分段,用
ConcurrentHashMap做锁对象池,这就是分段锁的思路
近年来不少团队把长事务里的大同步块拆成多个小锁,实测吞吐提升明显,不过这依赖具体业务,没有通用数字。
面试与排查场景:怎么验证锁真的生效了
排查死锁不用瞎猜,两个命令足够:
jstack <pid> > stack.txt # 找 "Found one Java-level deadlock" jconsole # 图形化看线程状态和锁持有
想看锁竞争激烈程度,可以开 -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 观察同步相关的内联情况,或者用 JFR 录制 jdk.JavaMonitorEnter 事件,直接统计每次等锁耗时,北京不少 Java 后端团队在面试里会追问“你怎么证明这段代码真的没锁竞争”,答案就是这些可复现的命令和事件,而不是背概念。
关于对象锁在虚拟机中实现的Q&A
Q:对象锁在虚拟机里是如何实现的,为什么不直接用操作系统互斥量?
因为纯内核互斥量每次加锁都要系统调用,单线程场景下开销太大,HotSpot 用 Mark Word 状态位加三级膨胀做“按需付费”:没竞争就不进内核,有竞争才挂起,这是性能与正确性的折中。
Q:偏向锁被移除了,是不是锁优化的知识就不用学了?
不是,JEP 374 移除的是偏向锁这一级,轻量级锁、重量级锁、自旋、锁消除、锁粗化都还在,JDK 17、21 上跑 synchronized,走的仍然是 Mark Word 加 ObjectMonitor 这套机制,只是少了“认主”环节,首次加锁直接从轻量级开始。
Q:为什么 synchronized 是可重入的?
因为重量级锁对应的 ObjectMonitor 里有 _recursions 计数,同一线程再次进入时该字段加一,退出时减一,归零才真正释放,轻量级锁对应的 Lock Record 也是按栈帧压入多个,天然支持重入。