bigywater.tech
Java · 342

类初始化死锁

类初始化死锁不报 BLOCKED,也不会被 ThreadMXBean 发现。这篇把它复现出来,看线程转储里的特征,量它的时间窗口,并看单线程循环初始化与初始化失败的行为。

null
ThreadMXBean.findDeadlockedThreads() 在类初始化死锁时的返回值(线程状态是 RUNNABLE)
40/40 → 0/40
T2 比 T1 晚启动 200 µs 与 5000 µs 时 40 轮里死锁的轮数( 里不等待)
NoClassDefFoundError
静态初始化器抛异常后,第 2 次起访问该类得到的错误

A 与 B 互相引用

实测 · 本机执行输出
  [A ↔ B]
    [   3 ms][T2] B.<clinit> 开始
    [   3 ms][T1] A.<clinit> 开始
    1.5 秒后 T1 状态 RUNNABLE,T2 状态 RUNNABLE(isAlive true/true)
    ThreadMXBean.findDeadlockedThreads() = null;findMonitorDeadlockedThreads() = null
      "T1" #20 [26371] daemon prio=5 os_prio=31 cpu=7.56ms elapsed=3.09s tid=0x0000007b9b0bea00 nid=26371 in Object.wait()
         java.lang.Thread.State: RUNNABLE
      	at A.<clinit>(A.java:1)
      	- waiting on the Class initialization monitor for B
      	at Main$$Lambda/0x0000009001000c10.run(Unknown Source)
      	at java.lang.Thread.runWith(java.base@21.0.12.1/Thread.java:1596)
      	at java.lang.Thread.run(java.base@21.0.12.1/Thread.java:1583)
      "T2" #21 [26115] daemon prio=5 os_prio=31 cpu=9.33ms elapsed=3.09s tid=0x0000007b9b0bf100 nid=26115 in Object.wait()
         java.lang.Thread.State: RUNNABLE
      	at B.<clinit>(B.java:1)
      	- waiting on the Class initialization monitor for A
      	at Main$$Lambda/0x0000009001001a08.run(Unknown Source)
      	at java.lang.Thread.runWith(java.base@21.0.12.1/Thread.java:1596)
      	at java.lang.Thread.run(java.base@21.0.12.1/Thread.java:1583)
    T1 的栈(Thread.getStackTrace):
        A.<clinit>(A.java:1)
        java.base/java.lang.Thread.run(Thread.java:1583)
    T2 的栈(Thread.getStackTrace):
        B.<clinit>(B.java:1)
        java.base/java.lang.Thread.run(Thread.java:1583)

Parent 与 Child:没有显式的环

实测 · 本机执行输出
  [Parent ↔ Child]
    [   3 ms][T1] Parent.<clinit> 开始(先停一会儿,再创建 Child)
    1.5 秒后 T1 状态 RUNNABLE,T2 状态 RUNNABLE(isAlive true/true)
    ThreadMXBean.findDeadlockedThreads() = null;findMonitorDeadlockedThreads() = null
      "T1" #20 [27395] daemon prio=5 os_prio=31 cpu=9.73ms elapsed=3.16s tid=0x00000079b70c6a00 nid=27395 in Object.wait()
         java.lang.Thread.State: RUNNABLE
      	at Parent.<clinit>(Parent.java:1)
      	- waiting on the Class initialization monitor for Child
      	at Main$$Lambda/0x0000008001000c10.run(Unknown Source)
      	at java.lang.Thread.runWith(java.base@21.0.12.1/Thread.java:1596)
      	at java.lang.Thread.run(java.base@21.0.12.1/Thread.java:1583)
      "T2" #21 [26883] daemon prio=5 os_prio=31 cpu=0.25ms elapsed=3.10s tid=0x00000079b70c7100 nid=26883 in Object.wait()
         java.lang.Thread.State: RUNNABLE
      	at java.lang.Class.forName0(java.base@21.0.12.1/Native Method)
      	- waiting on the Class initialization monitor for Parent
      	at java.lang.Class.forName(java.base@21.0.12.1/Class.java:423)
      	at java.lang.Class.forName(java.base@21.0.12.1/Class.java:414)
      	at Main.lambda$main$0(Main.java:27)
      	at Main$$Lambda/0x0000008001001400.run(Unknown Source)
      	at java.lang.Thread.runWith(java.base@21.0.12.1/Thread.java:1596)
      	at java.lang.Thread.run(java.base@21.0.12.1/Thread.java:1583)
    T1 的栈(Thread.getStackTrace):
        Parent.<clinit>(Parent.java:1)
        java.base/java.lang.Thread.run(Thread.java:1583)
    T2 的栈(Thread.getStackTrace):
        java.base/java.lang.Class.forName0(Native Method)
        java.base/java.lang.Class.forName(Class.java:423)
        java.base/java.lang.Class.forName(Class.java:414)
        Main.lambda$main$0(Main.java:27)
        java.base/java.lang.Thread.run(Thread.java:1583)

单线程里的循环初始化

实测 · 本机执行输出
  [单线程]
    [   0 ms][main] 单线程:先访问 X
    [  13 ms][main] Y() 构造时 X.s = null
    [  13 ms][main] new X().get() = x-ready

初始化失败之后

实测 · 本机执行输出
  [Bad]
    [   0 ms][main] 第 1 次访问 Bad:java.lang.ExceptionInInitializerError  ← cause: java.lang.NumberFormatException: For input string: "not a number"
    [  13 ms][main] 第 2 次访问 Bad:java.lang.NoClassDefFoundError: Could not initialize class Bad  ← cause: java.lang.ExceptionInInitializerError: Exception java.lang.NumberFormatException: For input strin
    [  13 ms][main] 第 3 次访问 Bad:java.lang.NoClassDefFoundError: Could not initialize class Bad  ← cause: java.lang.ExceptionInInitializerError: Exception java.lang.NumberFormatException: For input strin

时间窗口有多大

实测 · 本机执行输出
  [T2 晚 0 µs]
    <clinit> 里等待 0 ms,T2 比 T1 晚 0 µs 启动:40 轮中死锁 40 轮
  [T2 晚 200 µs]
    <clinit> 里等待 0 ms,T2 比 T1 晚 200 µs 启动:40 轮中死锁 40 轮
  [T2 晚 1000 µs]
    <clinit> 里等待 0 ms,T2 比 T1 晚 1000 µs 启动:40 轮中死锁 4 轮
  [T2 晚 5000 µs]
    <clinit> 里等待 0 ms,T2 比 T1 晚 5000 µs 启动:40 轮中死锁 0 轮
  [T2 晚 20000 µs]
    <clinit> 里等待 0 ms,T2 比 T1 晚 20000 µs 启动:40 轮中死锁 0 轮
  (<clinit> 里等待 100 ms 的对照)
  [等待 100 ms,晚 5000 µs]
    <clinit> 里等待 100 ms,T2 比 T1 晚 5000 µs 启动:40 轮中死锁 40 轮

拆开依赖

实测 · 本机执行输出
  [Holder]
    [   3 ms][T1] T1 得到 holder-0
    [   3 ms][T2] T2 得到 holder-0
    [  15 ms][main] T1 alive=false T2 alive=false

分步演示

类初始化死锁
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;线程转储取自 DiagnosticCommand threadPrint(与 jstack/jcmd Thread.print 同源,没有另外运行 jstack 命令);第 5 节每轮用新的类加载器重新加载 A、B,死锁轮数是 40 轮里的计数,具体数字每次运行会有抖动
    • 没有验证:JDK 17 与 27 上线程转储里是否也是 “Class initialization monitor” 这一行(没有运行)、用 jstack 命令行(而不是 DiagnosticCommand)直接抓取、真实项目里的初始化死锁(只用了构造的最小例子)、第 6 节只是拆掉了环并没有改造一个真实的循环依赖
    京ICP备2026067502号-1京公网安备11010502063487号