深入理解ClassLoadingLock

简介: 上回书说道,我们在java.lang.ClassLoader中的loadClass方法中看到有类加载相关的并发控制,那么它到底是为了解决什么样的问题,以及它是如何使用的呢?

Preface

image.png
上回书说道,我们在java.lang.ClassLoader中的loadClass方法中看到有类加载相关的并发控制,那么它到底是为了解决什么样的问题,以及它是如何使用的呢?

LoadClassMethod

在jdk1.7以前,java.lang.ClassLoader的一些核心的方法都是被synchronized修饰的,比如像如下的loadClass方法。

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //.....
}

而在jdk1.8中,java.lang.ClassLoader对一些核心的方法进行了优化,其代码如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        //....
    }
}

Why change?

在上文我们所讲到的"双亲委派"的机制下,其层级都是树形的,你可能听说过Tomcat6之前多层级类加载器。但是它其实也按照"双亲委派"的机制实现的(它也可以打破"双亲委派"机制,关键点在于WebAppClassLoader,本篇不展开讨论,详情见笔者<<Tomcat如何打破双亲委派机制>>),如下所示。
image.png
但是也有例外的场景,就拿OSGI(Open Service Gateway Initiative)来说,它是一个基于Java平台的动态模块化规范,OSGI中的每个模块(Bundle)可以声明所依赖的JavaPackage,也可以声明它允许导出发布的JavaPackage,每个模块从表面看来为平级之间的依赖。OSGI其中一个很大的优点就是基于OSGI的程序很可能可以实现模块级别的热插拔功能,其背后所对应的就是它灵活的类加载器的架构。
而OSGI采取了不同的类加载机制保证每个模块都有自己独立的classpath,它为每个Bundle提供一个类加载器,该加载器能够看到Bundle内部的元数据资源。Bundle可以基于依赖关系互相协作,可以从一个Bundle类加载器委托到另一个Bundle类加载器。
例如BundleA依赖Java的核心类库,它委托给BootstrapClassLoader类加载器。而BundleB依赖BundleA、BundleC、核心类库,BundleC依赖BundleA,那么它们的关系图如下:
image.png
为什么花大篇幅说明这个呢?且听继续讲解,从上文我们可以看到,OSGI不在是"双亲委派"的树形的加载机制,而是演进为一种在运行时的更为复杂的"网格委派"机制,通过上文得知,在jdk1.7以前loadClass等核心方法都是被synchronized修饰的,所以就会产生一个问题,当BundleA依赖BundleB,并且BundleB依赖BundleA的时候,BundleA锁定当前类加载对象,然后委派给BundleB。BundleB锁定当前类加载器对象,然后委派给BundleA,就造成了死锁,如下。
image.png

ClassLoadingLock

所以说,在jdk1.7中针对此上的网状型类加载结构做了优化,就是本文所要讲的ClassLoadingLock。其通过手动注册的方式将classLoader标识为具有并行能力。其后在loadClass的时候废弃方法级别synchronized的方式,改为了为每个要加载的class文件(全限定名)都对应一个Object对象锁。而当加锁的时候如果此类加载器具有并行能力,那么就不再对这个类加载器进行加锁,而是找到这个类所对应的Object对象进行加锁操作。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 类加载的并发控制锁
        synchronized (getClassLoadingLock(name)) {
            //....
        }
}
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
 }

AppClassLoader

这里拿AppClassLoader来举例说明,其依赖关系如下。
image.png
静态方法注册AppClassLoader为并行处理类

static class AppClassLoader extends URLClassLoader {
    static {
        // 注册此类加载器为并行处理
        ClassLoader.registerAsParallelCapable();
    }
    //....
}

而ClassLoader.registerAsParallelCapable()是ClassLoader的静态方法,其中会调用ParallelLoaders.register方法,ParallelLoaders是ClassLoader的一个静态内部类,其中包含一个Set类型的静态属性,上面的注册操作会把类加载器添加到其中:

@CallerSensitive
protected static boolean registerAsParallelCapable() {
    // 获取调用者类,并判断是否是ClassLoader的子类
    Class<? extends ClassLoader> callerClass = 
        Reflection.getCallerClass().asSubclass(ClassLoader.class);
    // 将调用者类进行注册
    return ParallelLoaders.register(callerClass);
}
// 存放具备并行能力的类加载器容器
private static final Set<Class<? extends ClassLoader>> loaderTypes =
    Collections.newSetFromMap(
        new WeakHashMap<Class<? extends ClassLoader>, Boolean>()
    );

// 具体的注册方法
static boolean register(Class<? extends ClassLoader> c) {
    synchronized (loaderTypes) {
        if (loaderTypes.contains(c.getSuperclass())) {
            loaderTypes.add(c);
            return true;
        } else {
            return false;
        }
    }
}

到此AppClassLoader通过静态方法注册到loaderTypes中,而java的类加载过程中如果父类有静态代码块的话,先执行父类的静态代码块,再执行子类的静态代码块,然后执行父类的无参构造,再执行子类的构造方法。其顶层父类的无参构造如下:

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        // 判断是否注册为具有并行处理能力的类加载器
        if (ParallelLoaders.isRegistered(this.getClass())) {
            // 用于存放锁对象,key:需要加载的字节码文件名。value:Object锁对象:
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
}
static boolean isRegistered(Class<? extends ClassLoader> c) {
    synchronized (loaderTypes) {
        return loaderTypes.contains(c);
    }
}

到这里,为什么loadClass中调用getClassLoadingLock(name)就一目了然了。

protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
}

小结

经过此上分析,我们不难得出,如果某一个类加载器注册了具备并行能力,其会在初始化的时候创建了parallelLockMap用来存储类的全限定名与锁对象,也就是说会为每一个对应的className都创建一个Object锁对象,如果没有注册为并行处理的类加载器,其还是使用类加载器本身。

目录
相关文章
|
7月前
|
存储 Docker 容器
Docker安装默认存储路径修改与镜像恢复
Docker安装默认存储路径修改与镜像恢复
232 0
|
7月前
|
Cloud Native Go 项目管理
敏捷项目管理解锁:2023年使用YouTrack的全面指南
敏捷项目管理解锁:2023年使用YouTrack的全面指南
353 0
|
JavaScript 搜索推荐 Java
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)(上)
一次偶然的机会,让我遇见了amis之排错总结(持续更新,因为还在学习)
|
11月前
|
XML Java 数据格式
spring-boot集成spring-brick实现动态插件
spring-boot集成spring-brick实现动态插件
859 0
|
8月前
element-ui的upload组件的clearFiles方法的调用
element-ui的upload组件的clearFiles方法的调用
300 0
|
9月前
|
Java 数据库连接 API
OceanBase数据库中,确实存在三个不同的Driver
OceanBase数据库中,确实存在三个不同的Driver
552 1
|
10月前
|
IDE Java 应用服务中间件
以Gradle插件的方式为Java web项目启动Tomcat
在社区版IntelliJ IDEA除了用SmartTomcat,还有什么方式可以在可调试的情况下启动Tomcat呢,来试试com.bmuschko.tomcat插件吧
289 0
以Gradle插件的方式为Java web项目启动Tomcat
|
11月前
|
存储 Java
Java中方法、字段名的最大长度是多少?
由于Class文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名称,所以 CONSTANT_Utf8_info 型常量的最大长度也就是 Java 中方法、字段名的最大长度。
236 0
|
11月前
|
JSON 监控 JavaScript
|
11月前
|
前端开发 JavaScript UED
如何在 React 中实现鼠标悬停显示文本?
如何在 React 中实现鼠标悬停显示文本?
242 0