偏向锁的Epoch优化机制

偏向锁 Epoch 机制:如何判断"旧值"、和谁比、存在哪(附 JDK 15+ 时效警告)

关键词:偏向锁 / epoch / 批量重偏向 / 批量撤销 / 无锁 / STW / JDK 15 废除

适用版本:JDK 6--14(默认启用);JDK 15 默认禁用(JEP 374);JDK 18 移除实现;JDK 19+ 标志过期。这是历史机制,但面试常考且易答错。


一、背景(为什么需要 epoch)

偏向锁(Biased Locking)的核心假设是:大多数锁在生命周期内只被同一个线程 反复进入。于是 HotSpot 把"锁"退化成"在对象头里记一个线程指针",该线程再进临界区时零 CAS、零原子指令,直接进。

但问题来了:偏向锁的撤销(revoke)必须走 STW(safepoint)。撤销要把对象从"偏向 T1"改成"无锁/轻量",必须等全局安全点才能动 Mark Word,否则会和正在跑的线程看到的锁状态冲突。

于是出现一个反直觉的坑:

若某个类有 1000 个对象,先都偏向 T1,后来 T2 轮流访问这 1000 个对象------每个对象的第一次访问都会触发一次撤销 STW。1000 次 STW,偏向锁反而比轻量级锁更慢。

epoch 机制正是为把"逐对象、多次 STW"摊薄成"几次类级 STW + 大量廉价 CAS"而设计的。它是偏向锁的自适应优化。


二、初步了解(epoch 机制是什么)

2.1 核心三句话

  1. 每个 类(Klass 元数据) 持有一个 epoch 版本号;
  2. 对象被偏向时,把"当前类的 epoch"拷进自己的 Mark Word;
  3. 当某类的偏向撤销累计达到阈值,JVM 给这个类换一个新的 epoch,并批量作废旧偏向------后续该类旧对象"逻辑上"全部失效,无需逐个撤销。

2.2 两个阈值(自适应)

参数 默认 触发动作
BiasedLockingBulkRebiasThreshold 20 撤销累计达 20 → 批量重偏向(bulk rebias):类 epoch +1,旧偏向逻辑作废,后续访问走廉价 CAS 重偏向(无 STW)
BiasedLockingBulkRevokeThreshold 40 撤销进一步恶化达 40 → 批量撤销(bulk revoke) :该类被标记 biased-locking disabled,以后 new 出来的对象直接是无锁/轻量,不再走偏向逻辑
BiasedLockingDecayTime ~25000ms 衰减窗口:窗口内撤销次数没再冲破下一档阈值,计数器衰减归零,给类"改过自新"重新允许偏向的机会("自适应"含义)

2.3 一个易错表述(面试要讲准)

⚠️ 纠偏 :bulk rebias 不会"一次性把所有旧对象偏向到某个具体线程"。它的真实语义是:翻号让旧偏向集体作废(类 epoch 改变),之后哪个线程先访问该对象,就由哪个线程 CAS 把它重新偏向自己 。

所以"批量重偏向"=「批量作废旧偏向 + 允许廉价重偏向」,而非"指向当前线程"。这是高频口误。


三、深度了解(epoch 判旧:和谁比、存在哪、怎么判)

这是本文最关键的一层。

3.1 直接结论

判定规则:拿「对象自身 Mark Word 里的 epoch」与「它所属类的 prototype header(类元数据里的 epoch)」做比较。

  • 两者相等 → 偏向有效(且若偏向的是自己线程则零成本进临界区);
  • 两者不相等 → 偏向已作废(stale),可走廉价 CAS 重偏向 ,无需 STW 撤销。

3.2 epoch 存在哪两个位置

位置 存放处 说明
对象的 epoch(副本) 对象实例自己的 Mark Word 的 epoch 位域 对象被偏向时,从类的 prototype header 拷贝进来
类的 epoch(权威当前值) InstanceKlass 的 _prototype_header (一个 markOop,存在类元数据里,不在任何对象上) 类级"当前版本号";bulk rebias 改的就是它

核心要点:对象 epoch 是副本,类 prototype header 的 epoch 是权威版本号。比较就在"副本 vs 权威"之间发生。

3.3 对象 Mark Word 位布局(64 位 + 压缩指针)

偏向锁状态下的 Mark Word(源自 markOop.hpp 官方注释):

复制代码
JavaThread*:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2
  • epoch 只占 2 位(值域 0~3),不是想象中的"大计数器"。
  • 类的 prototype header 里同样是 2 位。所以"新/旧"只需区分两代:对象要么和类当前 epoch 同代(有效),要么隔代(作废)。
  • 为何 2 位够用:bulk rebias 由阈值门控(累计 20 次才翻一次),且 decay(~25s)会重置计数;同时存在的有效代差只有"当前代 + 上一代",2 位绰绰有余。

3.4 判旧流程(fast path 逐条)

线程访问对象 o 时,HotSpot 在偏向锁 fast path(BiasedLocking::revoke_and_rebias + 汇编)里:

  1. 读 o 的 Mark Word → 确认是 biased 模式(biased_lock=1, lock=01);
  2. 取 o.epoch(2 位);
  3. 取 o 所属类的 klass->prototype_header() 里的 epoch(权威当前值);
  4. 比较分支 :
    • o.epoch == klass.epoch 且偏向线程 == 当前线程 → 直接进临界区(零成本);
    • o.epoch == klass.epoch 但偏向的是别的线程 → 撤销(走 STW);
    • o.epoch != klass.epoch → 偏向已作废 → 廉价 CAS 重偏向(无 STW)。

这就是"批量重偏向省 STW"的底层支点:旧 epoch 对象一比对就发现代差,于是不走撤销、直接 CAS 认新主。epoch 不匹配,正是触发"免 STW 重偏向"的开关。

3.5 一个易错澄清

  • epoch 不是"对象自己记着被撤销了",而是"对象的版本号跟不上类的版本号"。类翻号(epoch+1),对象没跟着翻 → 代差 → 判旧。
  • 翻号动作本身是一次 STW(bulk rebias) ,但翻完之后所有旧对象的"失效"是零 STW 的懒检测------这就是 epoch 把"N 次逐对象撤销 STW"摊薄成"1 次类级 STW + 大量 CAS"的精髓。

四、源码了解(可溯源的官方依据)

内容 出处(OpenJDK)
偏向锁 Mark Word 位布局、epoch:2 位域 hotspot/share/oops/markOop.hpp(epoch_shift / epoch_mask_in_place)
类的当前 epoch 存放 Klass::_prototype_header,读 klass->prototype_header()
判旧(epoch 不匹配→rebias) hotspot/share/runtime/biasedLocking.cpp 的 BiasedLocking::revoke_and_rebias 内比对 prototype_header->epoch() 与 mark->epoch()
批量重偏向 / 批量撤销门控 biasedLocking.cpp 中 BiasedLockingBulkRebiasThreshold / BiasedLockingBulkRevokeThreshold 逻辑;阈值在 globals.hpp 定义(默认 20 / 40)
衰减机制 BiasedLockingDecayTime(默认 25000ms)
偏向锁启动延迟 BiasedLockingStartupDelay(默认 4000ms,即启动 4s 后才给对象偏向,避免启动期单线程预热的浪费)
JDK 15 默认禁用 JEP 374 --- Deprecate and Disable Biased Locking
JDK 18 移除实现 JDK-8256425(实现彻底移除,部分标志 obsolete)
JDK 19+ 标志过期 -XX:+UseBiasedLocking 过期,启动传入报 Unrecognized VM option 直接崩 JVM

事实核对:上述 JEP 374 / JDK-8256425 / 阈值默认值均已在 OpenJDK 官方文档与源码中核实;本系列《JDK7 HashMap 成环》一文经 java-source-verifier 同源核验流程确认,结论一致可信。

4.1 为什么现代 JDK 把它废了(JEP 374 原文依据)

现代应用大量使用线程池 + 并发框架 ,锁竞争频繁,偏向锁的撤销 STW 开销常常超过 它省下的 CAS 开销;且偏向锁给 HotSpot 同步子系统引入了巨量复杂代码,成为维护与优化障碍(JEP 374:"the performance gains seen in the past are far less evident today")。因此:

  • JDK 15:默认禁用 (仍可用 -XX:+UseBiasedLocking 显式开,但参数已 deprecated);
  • JDK 18:实现彻底移除;
  • JDK 19+:标志过期,传了直接崩。

五、demo 实现(如何实证看 epoch / bulk rebias)

⚠️ 前置条件 :偏向锁只在 JDK 8--14 存在且默认开启(JDK 15+ 已废)。下面的 demo 需在这代 JDK 上跑,并加 -XX:+PrintBiasedLockingStatistics 才能看到批量重偏向/撤销的计数证据。

5.1 运行参数

bash 复制代码
java -XX:+UseBiasedLocking \
     -XX:BiasedLockingStartupDelay=0 \          # 关掉启动延迟,立刻可偏向
     -XX:BiasedLockingBulkRebiasThreshold=20 \
     -XX:BiasedLockingBulkRevokeThreshold=40 \
     -XX:+PrintBiasedLockingStatistics \        # 打印偏向锁统计(含 bulk rebias/revoke 次数)
     -jar YourApp.jar

5.2 触发"批量重偏向"的示例程序

思路:构造一个类 C,让 T1 先 new 并进入大量对象(偏向 T1),再让 T2 轮流访问这些对象------当撤销累计越过 20,JVM 会触发一次 bulk rebias,统计里 Bulk rebias 计数 +1。

java 复制代码
public class BiasedLockEpochDemo {
    static class C {            // 演示用类,重点在"同类大量对象被换线程访问"
        private int x;
        public void inc() { x++; }
    }

    public static void main(String[] args) throws Exception {
        // 1) T1 预热:让一批对象偏向 T1
        List<C> list = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                C c = new C();
                c.inc();          // 偏向 T1
                list.add(c);
            }
        });
        t1.start(); t1.join();

        // 2) T2 轮流访问这些对象:触发撤销;越过阈值后触发 bulk rebias
        Thread t2 = new Thread(() -> {
            for (C c : list) {
                c.inc();          // 非偏向线程访问 → 撤销;累计达 20 触发 bulk rebias
            }
        });
        t2.start(); t2.join();

        System.out.println("done; 看 JVM 退出时打印的 BiasedLockingStatistics");
    }
}

5.3 看什么(证据)

程序正常退出时(或加 -Xlog 对应标签),JVM 会打印一段偏向锁统计,形如:

复制代码
BiasedLockingStatistics
  ...
  [<count> revocations, <count> bulk rebias, <count> bulk revoke]
  • 你应能看到 bulk rebias 计数 > 0,这正对应"类 epoch 被翻号、旧对象免 STW 重偏向"的发生;
  • 继续加大 T2 的竞争强度(更多对象 / 更多换线程访问),可进一步把 bulk revoke 计数推上去,对应"该类被标记禁用偏向"。

说明:epoch 字段本身在对象头里,直接用纯 Java 读不出来(需要 Unsafe + 对象头偏移解析,超出本 demo 范围)。PrintBiasedLockingStatistics 是官方提供的、最干净的实证入口,足以证明 epoch 驱动的批量机制真实存在。


六、总结(面试一口答)

  1. epoch 是什么 :类级版本号,存在 InstanceKlass::_prototype_header;对象偏向时把副本存进自己的 Mark Word(占 2 位)。
  2. 怎么判旧:比较「对象 Mark Word 的 epoch」与「类 prototype header 的 epoch」;不相等即作废,可免 STW 廉价 CAS 重偏向。
  3. 两个阈值 :撤销累计 20 → bulk rebias(翻号);累计 40 → bulk revoke(该类禁用偏向)。DecayTime 给衰减归零的"改过"机会。
  4. 纠正口误:bulk rebias 是"批量作废 + 允许重偏向",不是"一次性全部指向某线程"。
  5. 翻号也有 STW:但它每越一次阈值才发生一次,把 N 次逐对象撤销 STW 摊薄成"几次类级 STW + 大量 CAS"。
  6. ⚠️ 时效必说 :偏向锁只适用于 JDK 6--14;JDK 15 默认禁用(JEP 374),JDK 18 移除,JDK 19+ 标志过期。现代 JDK 下这段代码是历史知识,张口按"现在还默认开"答会露怯。
相关推荐
CRMEB系统商城1 小时前
家电零售的供应商协同与物流提效
java·开发语言·人工智能·小程序·1024程序员节
jason.zeng@15022071 小时前
Nginx 配置详解(静态 HTML、下载目录、API 反向代理三套场景)
java·vue.js·1024程序员节
鬼手点金1 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节
Shadow(⊙o⊙)1 小时前
LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路
java·服务器·前端·1024程序员节
王大师王文峰1 小时前
Java+AI 面试题(二):业务场景设计 10 例【客服机器人、合同审查、Text2SQL、告警降噪、成本优化】场景题答法与追问链路
java·开发语言·人工智能
弦儿2 小时前
后端 Web 实战:员工管理(新增员工 + 事务管理 + 文件上传)
java·后端
Mr-Wanter3 小时前
Spring Boot 接口签名实践
java·spring boot
denlaku3 小时前
JDK 22 新特性详解
java·开发语言
余槐i3 小时前
开虚拟线程后接口反而更慢:JDK 21 synchronized 锁住载体线程的排查
java·spring boot·redis·mysql·性能优化