Java synchronized 关键字详解:从字节码到对象头与锁升级
synchronized 是 Java 内置的互斥锁机制,底层基于 Monitor 实现。其核心原理涉及字节码指令、JVM 对象头状态管理以及硬件层面的 CAS 与内存屏障。
synchronized 底层原理(总结版)
synchronized 底层使用的是 Monitor,Monitor 被翻译为监视器,是由 JVM 提供,C++ 语言实现。
使用 javap -v xxx.class 反编译一段代码可以看到机器指令:
monitorenter:上锁开始的地方
monitorexit:解锁的地方
- 其中被
monitorenter 和 monitorexit 包围住的指令就是上锁的代码
- 第二个
monitorexit 是为了防止锁住的代码抛异常后不能及时释放锁
Monitor 内部具体的存储结构:
- Owner:存储当前获取锁的线程,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于 Blocked 状态的线程
- WaitSet:关联调用了 wait 方法的线程,处于 Waiting 状态的线程
具体的流程:
- 进入 synchronized 代码块时,先让 lock(对象锁)关联 monitor,然后判断 Owner 是否有线程持有
- 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
- 如果有线程持有,则让当前线程进入 EntryList 进行阻塞,如果 Owner 持有的线程已经释放了锁,在 EntryList 中的线程去竞争锁的持有权(非公平)
- 如果代码块中调用了 wait() 方法,则会进去 WaitSet 中进行等待
synchronized 底层原理(详解版)
synchronized 的底层原理可以从三个层面来看:字节码层面、JVM 底层实现 和 硬件层面。
1. 字节码层面:monitorenter 和 monitorexit
当我们使用 synchronized 关键字时,无论是修饰代码块还是方法,在编译后的字节码中都会生成对应的指令。
同步代码块
对于 synchronized(object) { ... },编译器会在同步代码块的前后分别生成 monitorenter 和 monitorexit 指令。
public void method() {
synchronized(obj) {
System.out.println("hello");
}
}
编译后的字节码大致如下:
public void method(); Code:
0: aload_0
1: getfield
4: dup
5: astore_1
6: monitorenter // 进入同步块,尝试获取锁
7: getstatic
10: ldc
12: invokevirtual
15: aload_1
16: monitorexit // 正常退出同步块,释放锁
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 异常退出同步块,释放锁 (确保在异常情况下也能释放锁)
23: aload_2
24: athrow
25: return
关键点:可以看到有两个 monitorexit 指令,第一个用于正常退出,第二个用于处理异常情况(隐藏在 finally 语义中),这确保了即使同步块内抛出异常,锁也能被正确释放。
同步方法
对于 synchronized 修饰的方法,方法常量池中会设置 ACC_SYNCHRONIZED 标志。
public synchronized void method() {
}
当方法调用时,调用指令(如 invokevirtual)会检查这个标志。如果设置了,执行线程会先尝试获取锁(对于实例方法是 this,对于静态方法是该类的 Class 对象),再执行方法体。方法执行完毕后,无论是正常返回还是异常抛出,都会自动释放锁。
小结:从字节码看,synchronized 的实现依赖于 monitorenter 和 monitorexit 这一对指令,或者方法的 ACC_SYNCHRONIZED 标志。
2. JVM 底层实现:对象头与 Monitor
monitorenter 和 monitorexit 指令背后的具体实现,是 JVM 的核心。其关键在于 Java 对象头 和 Monitor。
2.1 Java 对象头(Mark Word)
在 HotSpot 虚拟机中,每个 Java 对象在内存中存储的布局分为三部分:对象头、实例数据、对齐填充。
其中,对象头 是理解锁的关键。它包含两部分信息:
- Mark Word:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等。它是实现锁的'主战场'。
- Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
为了在极小的空间内存储尽可能多的信息,Mark Word 被设计成一个非固定的动态数据结构。它会根据对象的状态复用自己的存储空间。
关键点:注意最后 2 位(lock),它标识了对象的锁状态。锁的升级过程就体现在这 2 位的变化上。
2.2 Monitor(管程/监视器锁)
JVM 为每个对象都关联了一个内置的 Monitor(管程)。monitorenter 指令的本质就是尝试去获取这个对象对应的 Monitor。
一个 Monitor 由以下部分组成:
- Owner:当前持有该 Monitor 的线程。初始为 null。
- EntryList:处于 Blocked 状态的、等待锁的线程队列。当 Owner 释放锁时,JVM 会从 EntryList 中挑选一个线程来成为新的 Owner。
- WaitSet:处于 Waiting 状态的、调用了 Object.wait() 方法的线程队列。这些线程在等待其他线程的通知(notify/notifyAll)。
工作流程:
- 当线程执行到
monitorenter 指令时,会尝试进入(enter)该对象的 Monitor。
- 如果 Monitor 的 Owner 为 null,则该线程成功成为 Owner,并将锁的计数器 +1。
- 如果该线程已经是 Owner(可重入锁),它再次进入,锁计数器再次 +1。
- 如果 Owner 是其他线程,则当前线程会进入 EntryList,进入 BLOCKED 状态,直到 Owner 线程释放锁。
- 当线程执行
monitorexit 指令时,锁计数器 -1。当计数器减到 0 时,线程释放 Monitor,不再担任 Owner。然后,EntryList 中的线程会开始竞争锁。
3. 锁的升级与优化
在 Java 6 之前,synchronized 是一个重量级锁,性能较差,因为它依赖于操作系统的 Mutex Lock(互斥锁),需要进行用户态到内核态的切换,耗时较长。
为了减少这种性能开销,Java 6 引入了锁升级机制。synchronized 的锁状态从低到高分为四种,升级路径是单向的:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
3.1 偏向锁
- 目的:在没有竞争的情况下,消除整个同步操作。假设在大多数情况下,锁不仅不存在竞争,而且总是由同一线程多次获得。
- 原理:
- 当一个线程访问同步块时,会在对象头和栈帧中的锁记录里存储偏向的线程 ID。
- 以后该线程再次进入和退出同步块时,不需要进行 CAS 操作来加锁和解锁,只需简单测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
- 如果测试成功,表示线程已经获得了锁。
- 撤销:一旦出现另一个线程来尝试竞争锁,偏向模式就宣告结束。持有偏向锁的线程会被挂起,JVM 会撤销偏向锁,然后升级为轻量级锁。
注意:在 Java 15 之后,偏向锁被标记为废弃并默认禁用,因为维护其带来的收益已不如从前。但理解其原理依然重要。
3.2 轻量级锁
- 目的:在竞争不激烈('近交替执行')的情况下,避免直接使用重量级锁带来的性能消耗。
- 加锁过程:
- 在当前线程的栈帧中创建一个名为 锁记录 的空间。
- 将对象头的 Mark Word 复制到锁记录中(称为 Displaced Mark Word)。
- 线程尝试使用 CAS 操作将对象头的 Mark Word 替换为指向锁记录的指针。
- 如果成功,当前线程获得锁。并将对象 Mark Word 的最后 2 位设置为 00,表示轻量级锁状态。
- 如果失败,表示存在竞争(另一个线程也修改了 Mark Word)。
- 解锁过程:
- 使用 CAS 操作将 Displaced Mark Word 替换回对象头。
- 如果成功,则同步过程顺利完成。
- 如果失败,说明锁已经升级,需要释放锁的同时唤醒被挂起的线程。
3.3 重量级锁
- 触发条件:当轻量级锁竞争失败后,会自旋尝试获取锁一定次数(自旋锁)。如果自旋后依然失败,锁就会膨胀为重量级锁。
- 特点:
- 此时 Mark Word 中存储的是指向重量级锁(Monitor)的指针。
- 等待锁的线程都会进入 EntryList,进入 BLOCKED 状态。
- 依赖于操作系统底层的 Mutex Lock,需要进行用户态到内核态的切换,成本最高。
4. 硬件层面:内存屏障与 CAS
synchronized 的语义保证了原子性、可见性和有序性。
- 可见性与有序性:是通过在编译器和处理器层面插入 内存屏障 来实现的。在同步块开始时加 Load Barrier,在同步块结束时加 Store Barrier,强制将工作内存中的修改刷新到主内存,并禁止指令重排序。
- 原子性:对于简单的
monitorenter/monitorexit,由 Monitor 保证。对于锁升级过程中的状态变更(如轻量级锁的获取),则是通过 CAS 操作实现的。CAS 是一条 CPU 原子指令(cmpxchg),它保证了'比较 - 交换'操作的原子性。