目录
一、对象头(Object Header)
Mark Word 结构
二、Monitor(监视器锁)
Monitor 核心结构
锁获取流程
三、锁升级机制
1. 偏向锁(Biased Lock)
2. 轻量级锁(Lightweight Lock)
3. 重量级锁(Heavyweight Lock)
四、内存语义
五、优化机制
总结
在 Java 中,synchronized
关键字的底层实现原理基于 对象头(Object Header)、Monitor(监视器锁) 和 锁升级机制,其设计目标是兼顾线程安全与性能优化。以下是详细原理分析:
一、对象头(Object Header)
每个 Java 对象在内存中分为三部分:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)。
synchronized
的锁信息存储在对象头的 Mark Word 中(占 64 位 JVM 下 8 字节)。
Mark Word 结构
锁状态 | 存储内容(64 位示例) |
---|---|
无锁 | 对象哈希码(25bit)、分代年龄(4bit)、偏向模式(1bit)、锁标志位(2bit,值 01) |
偏向锁 | 持有偏向锁的线程ID(54bit)、偏向时间戳(2bit)、分代年龄(4bit)、锁标志位(01) |
轻量级锁 | 指向栈中锁记录(Lock Record)的指针(62bit)、锁标志位(00) |
重量级锁 | 指向 Monitor 对象的指针(62bit)、锁标志位(10) |
GC 标记 | 空(用于垃圾回收阶段,锁标志位 11) |
特点:
- Mark Word 的内容会随着锁状态动态变化。
- 锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
二、Monitor(监视器锁)
Monitor
是 synchronized
的底层同步机制,每个对象关联一个 Monitor(由 C++ 的 ObjectMonitor
实现)。
Monitor 核心结构
class ObjectMonitor {void* _header; // Mark Wordvoid* _owner; // 持有锁的线程intptr_t _recursions; // 重入次数WaitSet _WaitSet; // 等待队列(调用 wait() 的线程)EntryList _EntryList; // 阻塞队列(竞争锁失败的线程)// ...
};
锁获取流程
- 当线程尝试获取锁时,若锁未被占用(
_owner
为空),则 CAS 设置_owner
为当前线程,获取成功。 - 若锁已被占用,线程进入
_EntryList
队列阻塞等待,等待锁释放后被唤醒。
三、锁升级机制
JDK 6 后引入 锁膨胀(Lock Inflation) 机制,根据竞争激烈程度动态调整锁状态,优化性能。
1. 偏向锁(Biased Lock)
- 目的:减少无竞争时的同步开销。
- 触发条件:对象未被锁定且未禁用偏向模式。JVM默认延时4s自动开启偏向锁,可通过-XX:BiasedLockingStartupDelay=0 取消延时;如果不要偏向锁,可通过-XX:-UseBiasedLocking =false来设置。
- 流程:
线程通过 CAS 将 Mark Word 中的线程ID设置为自己的ID,后续可直接进入同步代码块,无需竞争。 - 撤销:当其他线程尝试获取锁时,偏向锁会升级为轻量级锁。
2. 轻量级锁(Lightweight Lock)
- 目的:减少多线程交替执行时的锁竞争开销。
- 流程:
线程在栈帧中创建Lock Record
,通过 CAS 将 Mark Word 复制到 Lock Record,并尝试将 Mark Word 指向 Lock Record。
若成功则获取锁,失败则自旋重试或升级为重量级锁。
3. 重量级锁(Heavyweight Lock)
- 触发条件:自旋失败或竞争激烈。当线程锁竞争情况严重,jdk使用适应性自旋,某个达到最大自旋次数的线程(默认为10次),会将轻量级 锁升级Q为重量级锁(通过CAS修改所标志位,但不修改持有ID)。当后序的线程尝试获取锁时,就将自己挂起,等待被唤醒。重量级锁将控制权交给了操作系统,有操作系统来负责线程间的调度和状态变换,会出现频繁的对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。
- 机制:通过操作系统互斥量(Mutex Lock)实现线程阻塞和唤醒,涉及用户态到内核态的切换,性能开销大。
四、内存语义
synchronized
通过 锁的获取与释放 实现内存可见性:
- 进入同步块(获取锁):强制从主内存重新加载变量。
- 退出同步块(释放锁):强制将修改刷新到主内存。
五、优化机制
JDK 6 后引入多项优化:
- 锁消除(Lock Elimination):
- JIT 编译器通过逃逸分析,移除不可能存在竞争的锁。对象未逃逸:锁对象的作用域仅限于当前方法,不会被其他线程访问。无实际竞争:同步操作在多线程环境下没有实际意义。举个例子:
逃逸分析:StringBuffer 对象 sb 是方法内的局部变量,未逃逸到 concatStrings 方法外(不会被其他线程访问)。
锁消除:JIT 编译器会移除 sb.append() 内部的 synchronized 锁,优化后的代码等效于使用 StringBuilder(非线程安全类)。
public class LockEliminationDemo {// 方法内部的局部变量,未逃逸到方法外public static String concatStrings(int n) {StringBuffer sb = new StringBuffer(); // 锁对象 sb 未逃逸for (int i = 0; i < n; i++) {sb.append(i); // append() 方法有 synchronized 修饰}return sb.toString();}public static void main(String[] args) {String result = concatStrings(10000);System.out.println(result);} }
- 锁粗化(Lock Coarsening):将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。例子:
public void example() {Object lock = new Object();synchronized (lock) {// 操作1}synchronized (lock) { // 同一锁对象// 操作2} }******************优化后(合并为一个同步块)********************** public void example() {Object lock = new Object();synchronized (lock) {// 操作1// 操作2} }
for (int i = 0; i < 1000; i++) {synchronized (lock) {x += i; // 循环内重复加锁} }************优化后(锁提升到循环外)******************* synchronized (lock) {for (int i = 0; i < 1000; i++) {x += i; // 单次加锁} }
- 自适应自旋(Adaptive Spinning):根据历史自旋成功率动态调整自旋次数。从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式-一适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
总结
- 底层核心:通过对象头的 Mark Word 和 Monitor 实现锁状态管理。
- 锁升级:根据竞争强度动态调整锁类型(偏向锁 → 轻量级锁 → 重量级锁)。
- 设计目标:在无竞争时降低开销,在竞争激烈时保证线程安全。
- 适用场景:适合需要保证原子性、可见性和有序性的复杂同步逻辑。