java 类加载死锁通常由两个类在静态初始化块中相互引用对方,导致线程 a 等待类 b 的加载完成,而线程 b 等待类 a 的加载完成。具体做法是创建两个包含静态代码块的类,并在各自的静态块中实例化对方的类,从而触发 classloader 内部的同步机制产生死锁。

如何构造相互引用的类以触发死锁
要复现这种死锁,需要定义两个类,例如 ClassA 和 ClassB。关键在于它们的静态初始化块(static block)必须尝试访问对方的类。当 JVM 尝试加载这两个类时,会启动两个线程分别去加载它们。由于类加载器使用内置锁来保证线程安全,当一个线程持有类 A 的锁并试图加载类 B 时,如果类 B 的加载线程已经持有类 B 的锁并试图加载类 A,就会形成循环等待。

public class DeadlockDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 触发 ClassA 的加载
Class.forName("com.example.ClassA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
// 触发 ClassB 的加载
Class.forName("com.example.ClassB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
package com.example;
public class ClassA {
static {
System.out.println("Initializing ClassA");
try {
// 尝试加载 ClassB,这会触发 ClassB 的静态初始化
Class.forName("com.example.ClassB");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.example;
public class ClassB {
static {
System.out.println("Initializing ClassB");
try {
// 尝试加载 ClassA,这会触发 ClassA 的静态初始化
Class.forName("com.example.ClassA");
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行上述 DeadlockDemo 后,控制台通常会打印出 "Initializing ClassA" 和 "Initializing ClassB",然后程序挂起不再响应。这是因为 t1 线程在加载 ClassA 时被阻塞,等待 ClassB 加载完成;而 t2 线程在加载 ClassB 时被阻塞,等待 ClassA 加载完成。JVM 的类加载机制无法打破这种依赖关系,从而导致死锁。
为什么类加载器会导致这种同步问题
类加载器在加载类时会获取该类的监视器锁(monitor lock),以确保类的初始化过程是线程安全的。在 Class.forName() 或类首次被引用时,JVM 会检查该类是否已加载。如果未加载,则开始加载流程。在静态初始化阶段,JVM 会锁定该类对象。如果在静态初始化过程中又触发了对其他类的加载请求,且这些类也在被其他线程加载,就可能发生锁竞争。由于每个类加载操作都持有了自己的锁并等待对方的锁释放,因此形成了死锁条件。这种现象在并发环境下尤为常见,尤其是在多线程同时触发不同类的加载时。

// 验证死锁的代码片段
Thread t1 = new Thread(() -> {
synchronized (ClassA.class) {
System.out.println("Thread 1 holds lock on ClassA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (ClassB.class) {
System.out.println("Thread 1 holds both locks");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (ClassB.class) {
System.out.println("Thread 2 holds lock on ClassB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (ClassA.class) {
System.out.println("Thread 2 holds both locks");
}
}
});
// 注意:上面的代码是手动模拟死锁,实际类加载死锁由 JVM 内部机制自动处理
// 但原理相同:两个线程分别持有 ClassA 和 ClassB 的锁,并尝试获取对方的锁
通过观察线程 dump 或使用 jstack 工具,可以清晰地看到两个线程处于 WAITING 状态,并且互相持有对方需要的锁。这是典型的死锁特征。在实际开发中,应避免在静态初始化块中进行复杂的类加载操作,或者确保类之间的依赖关系是单向的,以避免此类问题的发生。
如何检测并解决类加载死锁
当怀疑存在类加载死锁时,可以使用 jstack 命令生成线程快照,分析线程状态。如果看到多个线程处于 BLOCKED 或 WAITING 状态,并且锁持有者之间存在循环依赖,即可确认为死锁。解决此类问题的方法包括:重构类结构,避免静态初始化块中的相互引用;使用延迟加载(Lazy Loading)技术,将类的初始化推迟到真正需要时;或者使用自定义类加载器,隔离不同模块的类加载过程,减少交叉依赖。此外,确保类加载顺序的一致性也有助于预防死锁的发生。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











