synchronized中偏向锁的“撤销”、“重偏向”和“批量撤销”

82 阅读6分钟

背景:偏向锁为什么存在?

在部分情况下,一个同步代码块在程序运行期间,其实只有一个线程会反复访问它(即不存在锁竞争)。如果每次这个线程进入同步块都进行传统的互斥操作(比如操作系统层面的互斥量),开销是比较大的。

偏向锁的核心思想是: 如果一个锁被一个线程获得,那么这把锁就偏向这个线程,后续如果还是这个线程来请求锁,它无需任何同步操作(CAS)就直接进入,代价极小。这就像在锁上贴了个标签:“此锁归线程A所有”。

重要前提: 理解下面所有机制的前提是,你都知道它们发生在没有线程竞争或竞争不激烈的场景下。如果一直存在激烈竞争,JVM会直接升级为轻量锁或重量级锁,就不会走偏向锁这套逻辑。

1. 偏向锁的撤销

什么是撤销?

“撤销”是指把一个偏向锁的状态,退回到不可偏向的未锁定的状态,或者升级为轻量锁的过程。

什么时候触发撤销?

当另一个线程B来尝试获取这个已经被线程A偏向的锁时触发。这时,锁已经“名不副实”了,因为它偏向的线程A并不是现在想要获取它的线程B。

撤销的具体步骤:

1.检查与等待:首先JVM需要检查偏向的线程A是否仍然活着。

  • 如果线程A已经死了,那么好办。JVM会把锁对象的标记位改为无锁状态(01),然后允许线程B用CAS操作重新将其偏向自己。

  • 如果线程A还活着,JVM会等待线程A到达一个安全点(比如GC的时候,线程会暂停执行),然后检查线程A的栈帧。 2.遍历栈帧:在安全点,JVM会挂起线程A,然后遍历它的栈帧,看看它是否仍然持有这个锁(即是否还在同步块中)。

    情况一: 线程A已释放锁(不在同步块中):  这说明竞争是“过去式”了。JVM 简单地将锁对象头恢复为不可偏向的无锁状态(01) 。然后唤醒线程A和线程B,让它们继续竞争。接下来,线程A和B会进行标准的轻量级锁加锁流程(CAS竞争)。

    情况二:线程A仍持有锁(在同步块中):  这说明发生了真正的竞争。此时,偏向锁已经不适合了。JVM 会将锁升级为轻量级锁。具体做法是,在线程A的栈帧中创建一个锁记录(Lock Record),并将锁对象的对象头指向这个锁记录。这样,锁就从偏向锁升级到了轻量级锁状态。然后线程A和B会在这个轻量级锁层面上进行竞争(通常是线程B自旋等待)。

2. 偏向锁的重偏向

什么是重偏向?

“重偏向”是撤销的一种特殊优化。它不是在发生竞争时直接把锁变成非偏向状态,而是将锁的偏向权直接转让给另一个线程

什么时候触发重偏向?

当一个对象被多个线程交替访问,但在特定时间点内并没有发生竞争时,JVM会选择重偏向,而不是撤销。

如何实现?- Epoch 机制

JVM 为每个类维护了一个 epoch 值,同时每个偏向锁的对象头里也有一个 epoch 字段。
当满足重偏向条件时(比如,某个类的对象被不同线程访问了一定次数),JVM会执行一次批量重偏向。它会增加类的 epoch 值,然后更新所有当前正处于加锁状态(被持有)的该类型对象的 epoch(这需要STW,但在安全点进行)。之后,当线程释放锁,新的线程来获取锁时,如果发现对象头的 epoch 和类的 epoch 不一致,并且原持有线程已不活跃,它不会执行昂贵的撤销操作,而是直接通过CAS将锁重偏向给自己,并更新对象头的 epoch。

这部分有点不太好理解,可以简单的理解为,如果一个对象被多个线程交替访问(没有竞争的),JVM会通过Epoch这种机制来识别这种情况,后续有线程来获取这个对象的锁,批量重偏向允许将锁的偏向从一个线程直接转移到另一个线程,而不需要先撤销再重新偏向),后续会些Epoch机制的具体实现细节。

078b2f18-5b5a-419d-a5c8-a603e9217504.jpeg

简单比喻:
还是那个停车位。物业发现最近这个车位虽然是“A专用”,但经常是A用完B用,B用完C用,他们之间从不会同时出现(无竞争)。于是物业想了个办法:他们不摘牌子了,而是准备了很多可擦写的标签。A停完车开走了,B来了可以直接把标签上的“A”改成“B”,这个车位就变成B的专用了。这比每次都要摘牌子、立牌子(撤销)效率高。

3. 偏向锁的批量撤销

什么是批量撤销?
当JVM发现某个类的锁不仅存在竞争,而且竞争非常频繁,偏向锁反而成了累赘(因为每次都要做撤销操作,得不偿失)时,JVM会决定彻底放弃对这个类的对象使用偏向锁

什么时候触发批量撤销?
当对一个特定类的对象的偏向锁撤销次数达到一个阈值时(例如,默认40次),JVM就会认为这个类的锁是“高度竞争的”,不适合再用偏向锁。

发生了什么?
JVM会将该类的标志置为不可偏向。之后,所有为该类新创建的对象,其对象头都会直接标记为不可偏向的状态(01)。从此,这些新对象再也不会进入偏向锁模式,任何一个线程来加锁,都会直接走轻量级锁的流程。

注意:  批量撤销只影响之后新创建的对象。之前已经创建出来的、正处于偏向状态的对象,仍然会在下次发生竞争时进行单独的撤销。

总结与流程

我们可以把这三个过程看作JVM的一个自适应的学习过程:

  1. 初始状态:  一个类的对象默认是可偏向的。

  2. 发生竞争:

    • 偶尔一次竞争 -> 单独撤销(变无锁或升级轻量级锁)。
    • 检测到“交替访问”模式 -> 批量重偏向(优化撤销过程,直接转让偏向权)。
  3. 竞争加剧:  撤销次数过多 -> 批量撤销(判定该类不适合偏向锁,新对象直接禁用偏向)。

机制触发条件目的影响范围
撤销一个线程尝试获取被另一个线程偏向的锁处理单次的锁竞争单个锁对象
重偏向检测到线程交替访问同一对象但无冲突的模式优化频繁交替访问场景下的撤销开销一个类的所有对象(通过epoch)
批量撤销某个类的锁撤销次数达到阈值避免在高度竞争的场景下继续使用低效的偏向锁一个类未来创建的所有对象

重要提示:  自从Java 15开始,偏向锁已经被默认禁用了。