Java类加载器中的死锁问题

6
我已经编写了两个自定义类加载器来动态加载代码。
第一个类加载器可以从Jar文件中加载代码:
package com.customweb.build.bean.include;

import java.net.URL;
import java.net.URLClassLoader;

import com.customweb.build.process.ILeafClassLoader;

public class JarClassLoader extends URLClassLoader implements ILeafClassLoader {

    public JarClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    public Class<?> findClassWithoutCycles(String name) throws ClassNotFoundException {

        Class<?> c = findLoadedClass(name);
        if (c != null) {
            return c;
        }

        return findClass(name);
    }

    @Override
    protected Class<?> findClass(String qualifiedClassName) throws ClassNotFoundException {
        synchronized (this.getParent()) {
            synchronized (this) {
                return super.findClass(qualifiedClassName);
            }
        }
    }

    @Override
    public URL findResourceWithoutCycles(String name) {
        return super.findResource(name);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        synchronized (this.getParent()) {
            synchronized (this) {
                return super.loadClass(name);
            }
        }
    }

}

另一个类加载器采用多个类加载器来访问其他类加载器的类。在第一个类加载器初始化期间,我将此类加载器的实例设置为父级。为了打破循环,我使用“findClassWithoutCycles”方法。

package com.customweb.build.process;

import java.net.URL;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.List;

public class MultiClassLoader extends SecureClassLoader {

    private final List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();

    public MultiClassLoader(ClassLoader parent) {
        super(parent);
    }

    public void addClassLoader(ClassLoader loader) {
        this.classLoaders.add(loader);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        for (ClassLoader loader : classLoaders) {
            try {
                if (loader instanceof ILeafClassLoader) {
                    return ((ILeafClassLoader) loader).findClassWithoutCycles(name);
                } else {
                    return loader.loadClass(name);
                }
            } catch (ClassNotFoundException e) {
                // Ignore it, we try the next class loader.
            }
        }

        throw new ClassNotFoundException(name);
    }

    @Override
    protected URL findResource(String name) {

        for (ClassLoader loader : classLoaders) {
            URL url = null;
            if (loader instanceof ILeafClassLoader) {
                url = ((ILeafClassLoader) loader).findResourceWithoutCycles(name);
            } else {
                url = loader.getResource(name);
            }

            if (url != null) {
                return url;
            }
        }

        return null;
    }
}

但是当我使用这些类加载器时,大部分时间会出现死锁。我在此处粘贴了线程转储: http://pastebin.com/6wZKv4Y0 由于Java ClassLoader在一些方法中通过对$this进行同步来阻止线程,因此我尝试首先在MultiClassLoader上进行同步,然后在JarClassLoader上进行同步。当获取锁的顺序相同时,这应该可以防止任何死锁。但似乎在本地类加载例程中某个地方会获取类加载器的锁。 我得出这个结论是因为线程'pool-2-thread-8'被锁定在对象'0x00000007b0f7f710'上。但是在日志中,我看不到这个锁是由哪个线程获取的以及何时获取的。
如何找出哪个线程在对类加载器进行同步?
编辑: 我通过在调用MultiClassLoader的loadClass之前对所有类加载器进行同步来解决了这个问题。

为什么要在父类加载器上同步?我看不出有任何理由。 - Holger
在类的定义过程中,使用ClassLoader作为锁(synchronize(this))。当在MultiClassLoader中调用loadClass()时,也会同步此ClassLoader。这意味着:有些情况下,在JarClassLoader同步之前,父级(MultiClassLoader)会先进行同步。在这种情况下,可能会发生死锁。通过同步,可以避免这种死锁情况的发生。 - Thomas Hunziker
我不认为增加更多的同步机制能够避免死锁。你的问题证明了这个概念行不通。但是,我希望我的回答能够帮到你。 - Holger
1个回答

7
JVM在调用loadClass之前会锁定ClassLoader。如果通过JarClassLoader加载的类引用了另一个类,而JVM试图解析该引用,则它将直接访问创建该类的ClassLoader,锁定它并调用loadClass。但是,在再次锁定JarClassLoader之前,您正在尝试获取父加载器的锁。因此,两个锁的顺序不起作用。
但我没有看到任何需要同步的资源,因此我不认为这两个锁有任何原因。URLClassLoader继承的内部状态由其实现本身维护。
但是,如果您想添加需要同步的类的更多状态,则应使用不同的机制来锁定ClassLoader实例。 http://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html提到了这一点。
如果您拥有一个自定义类加载器,存在死锁的风险,在Java SE 7发布后,您可以遵循以下规则避免死锁:
1.确保您的自定义类加载器对并发类加载是多线程安全的。 a. 决定内部锁定方案。例如,java.lang.ClassLoader使用基于请求的类名称的锁定方案。 b. 删除仅在类加载器对象锁上的所有同步。 c. 确保关键部分对于加载不同类的多个线程是安全的。
2.在自定义类加载器的静态初始化程序中调用java.lang.ClassLoader的静态方法registerAsParallelCapable()。此注册表明您的自定义类加载器的所有实例都是多线程安全的。
3.检查该自定义类加载器扩展的所有类加载器类是否也在其类初始化程序中调用了registerAsParallelCapable()方法。确保它们对于并发类加载是多线程安全的。
如果您的自定义类加载器仅覆盖findClass(String),则无需进一步更改。这是创建自定义类加载器的推荐机制。

@Holgar:你的回答非常有帮助。我之前不知道JVM在加载类时会锁定类加载器。但是使用registerAsParallelCapable()并没有成功。在调用loadClass()之前,我对MultiClassLoader中的所有类加载器进行了锁定。这样可以保持获取锁的顺序。 - Thomas Hunziker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接