双亲委派机制本身不提供线程安全,它仅规定类加载的委托顺序;真正的线程安全需由jvm类初始化同步保障及自定义classloader中显式并发控制(如concurrenthashmap或细粒度锁)来实现。

双亲委派本身不解决多线程同步问题,它只是类加载的委托策略
很多人误以为“双亲委派机制能保证线程安全”,其实不是。双亲委派是 ClassLoader.loadClass() 方法里的一套查找顺序逻辑:先让父加载器尝试加载,失败了才自己加载。它不涉及锁、不加 synchronized、也不对共享状态做保护——它只是个“查类时先问长辈”的流程约定。
真正影响多线程下类加载行为的,是类加载器实例本身的线程使用方式和 JVM 对类初始化阶段(<clinit></clinit>)的同步保障:
- JVM 保证一个类的
<clinit></clinit>(静态初始化块和静态变量赋值)在多线程环境下只会执行一次,且由第一个触发初始化的线程完成,其余线程阻塞等待——这是 JVM 级别的内置同步,无需开发者干预 - 但
findClass(String)、defineClass(byte[])这些方法本身是无锁的;如果你自定义了类加载器并在其中缓存了Class对象或解析结果(比如用ConcurrentHashMap存 class 字节码),那这部分就得自己处理并发安全 - 多个线程同时首次访问同一个类(如
new MyClass()),不会导致重复初始化,但可能触发多次loadClass()调用——只要没破坏双亲委派链,最终仍会落到同一个ClassLoader实例上走标准流程
为什么自定义 ClassLoader 在多线程中容易出错
问题不在双亲委派,而在你写的子类里没考虑并发。典型错误是把类字节码缓存到普通 HashMap 或直接用成员变量暂存,然后在 findClass() 中读写——这会导致竞态条件或 ConcurrentModificationException。
常见现象包括:
- 同一个类被多次
defineClass(),抛出LinkageError: duplicate class definition - 缓存未命中后多个线程各自去加载并解析字节码,造成重复开销甚至不一致行为
- 类加载中途被中断(如 IO 异常),但缓存已写入半成品,后续线程取到损坏的
Class引用
正确做法是:在自定义加载器中,对关键共享结构(如字节码缓存、已加载类映射)使用线程安全容器,或对加载逻辑加锁(推荐细粒度锁,比如按类名 hash 分段锁,而非整个 loadClass() 方法加 synchronized)。
ClassLoader 的“线程安全”其实是分层的,别一概而论
不同层级的安全性来源完全不同:
- 类加载委托过程(双亲委派):无锁、无同步,纯逻辑调用,线程安全靠的是“只读”和“不可变路径”
-
类初始化(
<clinit></clinit>):JVM 自动加锁,每个类一个隐式锁,无需干预 - 自定义加载器内部状态(如资源定位、字节码缓存、解析上下文):完全由你负责,必须显式同步或用线程安全结构
例如,Spring Boot 的 LaunchedURLClassLoader 就用了 ConcurrentHashMap 缓存已加载的 Class,并在 findClass() 中用双重检查 + computeIfAbsent() 避免重复加载——这不是双亲委派给的,是你得亲手补上的。
实战建议:如何写一个线程安全的自定义 ClassLoader
别从零造轮子,优先复用 JDK 提供的线程安全原语。下面是最小可行方案:
- 继承
ClassLoader,重写findClass(String name),不要重写loadClass(String name)(否则可能绕过双亲委派) - 用
ConcurrentHashMap<string class>> classesCache = new ConcurrentHashMap()</string>缓存已加载的类 - 在
findClass()中先查缓存:Class> cached = classesCache.get(name);若为空,再加载字节码、调用defineClass(),最后用classesCache.putIfAbsent(name, clazz)写入 - 如果加载过程涉及外部资源(如网络、jar 包解压),注意这些操作本身是否线程安全;必要时对资源句柄加锁,而不是锁整个方法
记住:defineClass() 是线程安全的,它只负责把字节数组转成 Class 对象,但多次对同一类名调用会失败。所以“避免重复 define”这件事,得靠你自己控制调用时机。
最易被忽略的一点:即使你做了所有同步,如果多个线程用的是不同的 ClassLoader 实例(比如每次 new 一个),那它们之间的类对象永远不相等——getClass().getClassLoader() != other.getClass().getClassLoader(),连 instanceof 都会失效。这种“伪冲突”比同步问题更难调试。










