正确做法是按类全限定名细粒度加锁,即用ConcurrentHashMap缓存每个类名对应的唯一锁对象,并在读取字节码、defineClass、写入缓存等关键步骤中同步该锁,确保同一类名的加载过程原子执行。

在自定义类加载器中利用 synchronized 保证原子性,核心不是“给整个类加载过程加锁”,而是**精准锁定类加载的关键临界资源**——即真正决定类是否已加载、是否需重复加载、以及 defineClass 等关键操作的共享状态。类加载器本身(尤其是双亲委派链中的多个实例)天然具备多线程并发调用场景,若不加控制,可能引发重复定义、LinkageError 或返回不同 Class 实例等严重问题。
为什么不能直接 synchronized(this) 或 synchronized(ClassLoader.class)
自定义类加载器通常被多个线程并发使用(如 Web 容器热部署、插件系统动态加载)。但:
-
synchronized(this)只锁当前加载器实例 —— 若存在多个独立加载器实例(如每个插件一个),它们互不阻塞,无法防止同一类被不同加载器重复加载; -
synchronized(MyClassLoader.class)锁的是类对象 —— 若系统中有多个MyClassLoader子类实例或不同类名的加载器,该锁对它们无效;更严重的是,它会无差别阻塞所有静态同步方法,造成全局性能瓶颈,且与“按类名隔离”的实际需求不匹配。
正确做法:按类全限定名细粒度加锁
类加载的原子性关键在于:**对同一个类名(name),只允许一个线程完成从字节码读取 → defineClass → 缓存到 classes 映射的全过程**。推荐方案是使用一个线程安全的缓存 + 同步块锁定具体类名对应的锁对象:
- 声明一个私有 final 锁池:
private final Map<String, Object> classLocks = new ConcurrentHashMap<>(); - 在
loadClass方法中,先获取该类名对应的唯一锁对象:Object lock = classLocks.computeIfAbsent(name, k -> new Object()); - 用该
lock对象同步整个加载逻辑:synchronized (lock) { ... } - 加载成功后,可选择性地移除该锁(避免内存泄漏):
classLocks.remove(name);(注意:仅在确认不再需要重载时才移除)
必须同步的关键步骤
以下代码片段中,只有标 * 的部分属于必须原子执行的临界区:
立即学习“Java免费学习笔记(深入)”;
- 检查本地缓存:
Class<?> cached = findLoadedClass(name);(非必须同步,findLoadedClass本身线程安全) - * 调用父加载器失败后,确认本加载器尚未加载:
if (cached == null) { - * 读取字节码(如从 Jar/网络/磁盘)
- * 调用
defineClass(name, b, off, len)—— 此操作不可重入,重复调用同一字节码会抛ClassFormatError - * 将新 Class 写入本地缓存(如
classes.put(name, clazz)) }
这几步必须包裹在同一把锁下,否则可能出现两个线程都读到 cached == null,各自 define 出两个 Class 实例,违反 JVM 规范。
避开常见陷阱
以下写法看似同步,实则失效:
-
synchronized(new Object()) { ... }—— 每次新建对象,锁无效; -
synchronized(name.intern())——String.intern()全局共享,不同类名可能因字符串常量池复用而意外竞争; - 在
loadClass外层加synchronized方法 —— 粒度过粗,阻塞所有类加载请求,包括无关类名; - 忘记在异常路径中释放锁(
synchronized自动释放,无需担心;但若用ReentrantLock则必须finally unlock)。
不复杂但容易忽略:锁对象必须稳定、唯一、生命周期可控,且同步范围严格覆盖“判断-加载-缓存”三连动作。


















