史上最细Synchronized实现原理

synchronized实际上有三种实现状态,在锁升级过程中,会依次从偏向锁、轻量级锁到重量级锁演进。下面会介绍这三种锁状态的实现方式。

零、Mark Word(后简称MW)

Java的synchronized基本上都是围绕这个Mark Word实现的,Mark Word保存在对象头中,用来标记当前的锁状态,具体结构如下所示。JavaThread*表示线程的指针,LockRecord*表示指向LockRecord栈中锁记录的指针,ObjectMonitor*表示指向ObjectMonitor监视器的指针。需要注意的是,物理内存中,低位在右,高位在左,下面表格按照这个方向制作。

锁状态bit 63~10
(高位数据区)
bit 9~8
(重偏向时间戳)
bit 7
(未使用)
bit 6~3
(分代年龄)
bit 2
(偏向标志)
bit 1~0
(锁标志)
无锁

bit 63~41:全0留空

bit 40~10:hashCode

00分代年龄0 (未偏向)

01

(偏向锁

或无锁)

偏向锁

JavaThread*

所属线程的指针

存放批量重偏向的批次值0分代年龄1 (已偏向)

01

(偏向锁

或无锁)

轻量级锁

LockRecord*

栈中锁记录的指针

00

(轻量级锁)

重量级锁

ObjectMonitor*

ObjectMonitor锁监视器的指针

10

(重量级锁)

通过MW判断锁状态的伪代码(0b前缀表示二进制):

// 1. 原子读取整个 Mark Word
mark = object->mark();
// 2. 判断是否为偏向锁(检查最低 3 位是否等于 101)
if ((mark & 0b111) == 0b101) {  
    // 偏向锁路径:比较线程指针
    if (mark.thread == current_thread) {
        // 重入,直接进入
    } else {
        // 竞争,触发偏向撤销(STW)
    }
} 
// 3. 判断是否为轻量级锁(检查最低 2 位是否等于 00)
else if ((mark & 0b11) == 0b00) {  
    // 轻量级锁路径:CAS 自旋
} 
// 4. 判断是否为重量级锁(检查最低 2 位是否等于 10)
else if ((mark & 0b11) == 0b10) {  
    // 重量级锁路径:阻塞等待
} 
// 5. 剩下的情况:lock=01 且 biased_lock=0(无锁)
else {  
    // 无锁路径:尝试 CAS 抢占
}

一、偏向锁

偏向锁的实现方式,就是通过CAS方式将synchronized对象头的MW修改为当前线程的指针,线程重入该锁时,不需要再执行CAS,只需要比较MW是否为当前线程ID。对于长期只有一个线程获取的锁对象来说,这种偏向锁的性能消耗是非常低的。

具体过程如下,在初始状态时,锁对象的头MW标志位biased_lock为0未偏向,lock标志位为01偏向锁(101表示可偏向),满足伪代码的else条件,使用CAS将高位数据区置为线程指针,同时将biased_lock置位1。后续该线程再进入该锁时,只要判断Mark Word后三位为标记为101,则直接比较高位是否指向当前线程,是则获取锁成功。需要注意的是,这个MD操作就算线程释放锁也不会置为无锁状态(这就是为什么叫偏向的锁),从而不管是重入锁还是释放锁后重新进入都只需要比较MD,带来较小的性能消耗。

同时,对于当前线程来说,会在当前栈帧(或栈顶)中压入一个 Lock Record [ Lock Record_1: {obj=锁对象, Displaced=null} ]。第二次、第三次进入同一个 synchronized 块时(锁重入),JVM 不会再去修改对象头(因为 ID 已经匹配)。但在栈上,JVM 会继续压入新的 Lock Record,每个新记录依然是 obj 指向锁对象,Displaced Mark Word = null。这些记录的唯一作用就是“重入计数器”,每次退出同步代码块弹出一个,保证获取多少次就释放多少次,只要还拥有一个Lock Record就认为线程仍然持有该锁,这是为什么MD足已判断当前偏向锁为线程持有仍然需要压入Lock Record的原因。后续出现竞争时就可以通过这个判断是否线程还持有锁。

Lock Record 字段存储内容说明
obj(锁对象引用)指向 锁对象 的指针标记这个锁记录属于哪个对象。
Displaced Mark Word(置换标记字)null(空)这是关键区别! 偏向锁不保存原 Mark Word 副本,因为释放时不需要还原对象头(对象头里的线程 ID 留着不擦除)。

通过上面的伪代码不难看出,假设出现多个线程竞争,则若继续使用偏向锁不断改变偏向的线程,需要频繁STW,这带来的消耗比CAS自旋还高,所以如果出现竞争,则偏向锁就会升级为轻量级锁。

二、偏向锁升级为轻量级锁过程

第一阶段:检测到竞争(T2 入场)
假设T1先获取了偏向锁,现在T2也试图获取该对象的偏向锁,首先T2 执行 monitorenter,读取MW检查最低三位,发现最低 3 位是 101,判定当前处于偏向锁状态,然后对比线程指针发现指向 T1,而不是当前线程 T2。

触发撤销:JVM 判定发生了锁竞争,不再允许 T2 进行 CAS 自旋(因为在偏向锁协议下,对象头没有自旋计数器和状态位),而是立即进入 “偏向锁撤销(Revocation)” 流程,若T1仍然使用该锁对象则在撤销后升级为轻量级锁,若 T1 已退出,则将对象头恢复为无锁状态(biased_lock=0, lock=01),后续 T2 可以重新尝试获取偏向锁(相当于一次全新的偏向锁获取流程)。

第二阶段:请求全局安全点(STW)
这是偏向锁性能开销最大的来源。为了保证线程栈帧的稳定性,JVM 必须暂停所有应用线程(包括 T1 和 T2),到达一个全局安全点(Safe Point)。为什么必须 STW? 因为 JVM 需要遍历 T1 的调用栈,查找 obj 对应的锁记录。如果不停顿 T1,T1 的栈帧在不断变化(方法入栈出栈),遍历结果就不准确。

第三阶段:遍历 T1 栈帧(查找 Lock Record)
在 STW 期间,JVM 拿着锁对象 obj 的引用,去线程 T1 的栈中执行全栈扫描,遍历每一个栈帧检查每个栈帧中的 Lock Record(锁记录)列表,查看是否有某个 Lock Record 的 obj 字段指向了当前这个 obj 对象。

此时会出现两种结果(升级为轻量级锁走的是情况 2):

情况 1(T1 已退出,不升级):如果在 T1 栈中找不到匹配的 Lock Record,说明 T1 早已执行完同步块,只是对象头残留了 T1 的 ID(偏向锁特性)。此时 JVM 会将对象头恢复为无锁状态(biased_lock=0, lock=01,清空线程指针),然后唤醒 T2,让 T2 重新走“无锁抢锁”流程。

情况 2(T1 仍持有,升级):如果在 T1 栈中找到了匹配的 Lock Record,说明 T1 依然在执行同步代码块,锁确实被占用。此时,JVM 执行升级操作。

第四阶段:膨胀为轻量级锁(核心转换)
当确认 T1 仍持有锁后,JVM 在 STW 期间执行以下原子转换,在保留T1持有锁的情况下将锁结构从偏向锁转为轻量级锁。

1.填充 T1 的 Lock Record

将 T1 栈中那个匹配的 Lock Record 的 Displaced Mark Word字段(偏向锁时为空),填入当前对象头中原本的 Mark Word 的副本(即包含 T1 线程指针和 age/epoch 的那串原始位数据)。在轻量级锁升级为重量级锁时,MD依然没有位置保留这部分内容,所以会转移保存在 ObjectMonitor 的 _header 字段中。

2.修改 Mark Word转换为轻量级锁结构

将对象头的MD整体覆写最低 2 位设置为 00(轻量级锁标志)。高位(2~63 位):存储指向 T1 栈中那个 Lock Record 的指针(62 位地址)。此时,biased_lock 位(bit 2)已经被指针覆盖,不再独立存在。虽然对象头变成了轻量级锁,但 Lock Record 属于 T1,所以 JVM 依然认为 T1 是锁的持有者。T1 对此毫无感知,它继续执行同步块内的代码。

3.退出 STW 所有线程恢复运行。

第五阶段:T2 自旋等待(轻量级锁特性)
STW 结束后,线程 T2 再次读取锁对象的MD,发现最低两位是 00(轻量级锁),且高位指向了 T1 的栈帧。T2 知道锁被 T1 占用,于是进入轻量级锁的自旋 重试)流程,在自己的栈帧中创建 Lock Record,然后循环执行 CAS,尝试将对象头的指针改为指向自己的 Lock Record。在 T1释放之前,CAS 一直失败,T2 会自旋(消耗 CPU)等待 T1 释放。直到 T1 退出同步块,执行轻量级锁释放逻辑将对象头 CAS 还原为无锁状态,T2 才有机会抢到锁。

三、轻量级锁升级为重量级锁过程

第一阶段:触发膨胀决策(自适应自旋失败)
T2 在自旋过程中,JVM 的自适应自旋机制动态调整自旋次数,如果自旋次数达到阈值(或持有者 T1 长时间未释放锁,如发生 GC 或 I/O 阻塞),JVM 判定 “继续自旋的成功概率极低,且浪费 CPU 资源”,则触发膨胀(Inflate)指令:JVM 不再允许 T2 继续自旋,而是进入 ObjectSynchronizer::inflate() 方法,正式启动升级流程。

第二阶段:分配 ObjectMonitor 并 CAS 抢占(原子替换)
这是升级过程中最关键的竞态步骤,JVM 必须保证仅有一个线程成功执行膨胀。

执行 CAS 替换对象头:T2尝试通过CAS操作,将对象头的Mark Word从指向T1栈的指针,修改为一个特殊的中转值 INFLATING。

如果 CAS 成功:当前线程(T2)成为“膨胀执行者”,进入第三阶段(作为锁升级的唯一打工人)。

如果 CAS 失败:

1. 持有者 T1 已释放锁,对象回到无锁状态
这是完全可能发生的。T1 可能执行速度极快,在 T2 发起膨胀的瞬间已经完成了同步块,并通过 CAS 将 Mark Word 成功还原为无锁状态(01)。

此时,T2 的 CAS 会因 Mark Word 已不是“指向 T1 栈”而失败。处理逻辑很清晰:锁竞争已经消失,T2 无需再膨胀,而是直接尝试用 CAS 获取这个无锁对象即可,然后重新走一遍轻量级锁的加锁流程。

2. 其他线程已成功将锁膨胀为重量级锁
这是另一个常见情况。如果另一个线程 T3 抢先一步完成了膨胀,Mark Word 已经被改为重量级锁状态(10),指向一个 ObjectMonitor 对象。T2 的 CAS 会因此失败,此时 T2 会退出膨胀,直接使用这个现成的 ObjectMonitor,进入ObjectMonitor的_cxq。

3. 其他线程正在执行膨胀(INFLATING 状态)
如果 T3 抢先将 Mark Word 改为了正在膨胀状态(INFLATING),T2 的 CAS 同样会失败。这时 T2 会自旋等待,不断重新读取 Mark Word,直到T3 完成膨胀后变为 10(重量级),然后进入ObjectMonitor的_cxq。

第三阶段:迁移锁持有权(T1 的栈信息 → Monitor)

T2线程在堆中申请一块 C++ 对象内存分配给 ObjectMonitor 对象,并初始化其核心字段:_owner = null(暂未拥有者),_recursions = 0(重入计数归零),_EntryList = null(阻塞队列为空),_WaitSet = null(调用了Object.wait()的等待队列为空)
CAS 成功后,执行者(假设是 T2 抢到了 CAS)需要将锁的持有信息迁移到 ObjectMonitor ,其中就包括将锁重入次数从T1的栈Lock Record计数统计出来赋值给_recursions 。所以T2会通过MD指向的T1的锁记录主动遍历 T1 的栈帧(注意:此时对象头已被改为 INFLATING,如果 T1 恰好执行 monitorexit,会看到 INFLATING 并自旋等待,因此 T1 的栈是稳定的,T2 可以安全读取)。T2 统计 T1 栈中所有指向该锁对象的 Lock Record 数量,得到重入次数 count。

然后填充 ObjectMonitor:将 _owner 设置为 T1,将 _recursions 设置为 count(重入次数),将Lock Record中保存的原 Mark Word 中的 hashCode/age 等保存到 _header 字段,并将 _EntryList 初始化为空队列。至此,ObjectMonitor初始化完成。

最后,T2 执行最后一次 CAS,将对象头的MD从 INFLATING 修改为指向该 ObjectMonitor 的指针(lock=10),然后T2通过 ObjectMonitor::enter() 进入_cxq 去排队。

第四阶段:自旋线程的迁移(T2、T3 进入 _cxq)

当 T2 将对象头从 INFLATING 改为 10 后,所有自旋等待的线程都会从自旋中醒来:

线程醒来后的动作
T3(竞争线程)获取重新读取 Mark Word,发现是 10(重量级锁)。
立即退出 inflate()复用这个现成的 ObjectMonitor,然后调用 ObjectMonitor::enter() 进入重量级锁竞争流程。
T1(持有者)释放重新读取 Mark Word,发现是 10(重量级锁)。
放弃轻量级锁释放逻辑,转而调用 ObjectMonitor::exit(),若重入计数减一后为0则将 _owner 置为 null,并唤醒 _cxq/_EntryList 中的等待线程。

四、重量级锁的运作机制

只有当前持有锁的线程(_owner)才能操作 _EntryList。这是一个非常严格的规则,确保了队列操作的独占性和安全性。

1.线程进入_cxq队列

当一个线程(比如线程T2)尝试获取已被占用的锁时会被封装成一个 ObjectWaiter 节点。这个节点会被优先放入 _cxq(竞争队列) 中-。_cxq 是一个无锁的、LIFO(后进先出) 的单向链表,允许大量线程通过CAS操作快速、并发地进入,减少了竞争。放入 _cxq 的线程会被挂起(park),请注意,线程并不直接进入 _EntryList,_EntryList 中的线程都是从 _cxq “转移”过来的。这个“转移”动作,发生在持有锁的线程(比如线程A)释放锁时。当线程A执行完同步代码,准备释放锁时,它会检查 _EntryList 是否为空。如果为空,但 _cxq 不为空,线程A就会将整个 _cxq 队列转移到 _EntryList 中。

2._cxq转移到_EntryList

这是一个由锁的持有者线程执行的操作。_EntryList 是一个双向链表,并且为了保证操作的简单和快速,新转移来的节点通常被设计为以LIFO(后进先出)的方式插入到 _EntryList 的头部。

_cxq 与 _EntryList 的区别

特性_cxq (竞争队列)_EntryList (入口列表)
数据结构无锁单向链表 (LIFO)双向链表-
并发控制无锁并发 (CAS)仅锁持有者可操作
线程状态刚到达,已挂起等待被唤醒的挂起线程-
主要目的快速入队,减少竞争-有序出队,便于持有者管理

3.锁释放时如何唤醒线程

当持有锁的线程A完全释放锁(_recursions 减为0)后,需要唤醒下一个等待线程。这个唤醒顺序由 JVM 参数 QMode 控制。核心逻辑是优先从 _EntryList 中获取线程进行唤醒。

常见的策略有:

默认(QMode=0):如果 _EntryList 不为空,则从 _EntryList 中取出一个线程唤醒-。如果 _EntryList 为空,则将 _cxq 中的线程全部转移到 _EntryList,再从其中唤醒一个。

QMode=1:将 _cxq 中的线程转移到 _EntryList,并唤醒 _EntryList 中的线程。

QMode=2:直接唤醒 _cxq 中的线程,不经过 _EntryList。

其他:存在更多复杂的策略,甚至可以将 _EntryList 中的线程转移到 _cxq-。

五、synchronized不同锁机制的核心区别

synchronized锁升级顺序为 偏向锁 -> 轻量级锁 -> 重量级锁,我们都知道性能消耗是随着锁升级越来越大的。通过上面的解析不难看出,偏向锁在没有锁竞争时,每次进入锁(包括重入或者释放或重新进入),都只需要比较MD的值,这是性能最好的。轻量级锁需要并发线程不断地自旋尝试CAS,多次自旋CAS需要消耗一些性能,但是始终是用户态运行,线程也始终没有挂掉,所以次之。而重量级锁,真正有了线程上下文切换的过程,线程竞争时直接挂起等待,后面又要被唤起,这带来的内核调度性能消耗是最大的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值