`
QING____
  • 浏览: 2234169 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ClassLoader并发加载问题(JDK 7)

 
阅读更多

    在JDK 7之前,ClassLoader存在并发(多线程)加载时可能导致死锁的问题(deadlock);在JDK 7通过对ClassLoader类内部进行了增强和优化,解决了此风险。我们先了解一下这个问题发生的原因,以及JDK 7通过何种技巧又解决了此问题。

 

    我们开发自定义的ClassLoader,通常继承java.lang.ClassLoader类,通过重写findClass(String name) 或者直接重写loadClass(String name)来实现,当然实际而言loadClass方法最终面向调用者、以及其内部仍然会调用findClass方法。(备注,通常通过重新findClass方法)。此外,众所周知,ClassLoader通过“双亲委派”方式来查找、加载类,其“委派”这就是其parent加载器,自定义ClassLoader可以指定parent,默认parent为系统的启动类加载器(提醒:不是线程类的加载器,此处不再赘言)。

 

    引起死锁的原因:

    1)JDK 6中loadClass方法是同步的,且同步操作修饰在方法级别,这意味着同步锁将在classLoader实例上。

    2)因为双亲委派关系,多个自定义的classLoader可以使用不同的、相同的parent,甚至交叉;比如CL1将CL2作为parent,那么CL2也可以将CL1作为parent;本质上这种设计思想上,并没有错;只是,这种委派方式,在全局来看,并没有遵守“单向、层级的”委派关系。

    3)CL1、CL2两个加载器的实例(通常一种类加载器只有一个实例),分别在两个(或者多个)线程中并发的load类时,死锁可能就会发生;因为1)loadClass方法是同步的,所以Thread1使用CL1加载类时首先获取CL1实例锁、loadClass方法中还需要获取CL2的锁(调用CL2的loadClass),那么Thread2使用CL2加载类也出现相同的问题,只是对象锁的顺序不同。

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

 

    有关deadLock的示例,可以参考:https://www.ibm.com/developerworks/library/j-dclp4/index.html

 

    即使他们并发加载不同的class,仍然有死锁的风险。所以,很多设计者,要么将自定义的classLoader委派关系层级设计的很“长”,要么就是彻底单线程加载,尽管此时加载的速度很慢,比如Tomcat 7之前的设计(Tomcat启动加载速度很慢)。

 

 

    其实JDK本身解决这个问题并不复杂,却懒散的拖延到了JDK 7才算考虑解决。

    1)移除了loadClass方法级别的同步,转而在内部设计了一个私有的(非静态)concurrentHashMap,K为className,V为Object对象锁;在执行loadClass时,首先检测map中是否已经有此className的对象锁,如果有则根据此对象锁进行同步,如果没有则创建。简单而言,就是降低了锁的粒度;此后,即使交叉委派,也不会再出现死锁的问题。

    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;
    }

 

    2)为了向后兼容,JDK并没有默认开启并发加载特性,我们需要关注一个方法:

    protected static boolean registerAsParallelCapable()

    此方法在JDK 7新增,表示将此classLoader注册为“并发的”;只有调用了此方法,1)中的concurrentHashMap才会被初始化,即支持className级别的并发锁;否则(map == null)仍然将classLoader实例作为同步对象。

    此方法,需要在所有的自定义ClassLoader中都要调用(包括委派的parent是自定义时),否则将不会生效。

public class CustomizeClassLoader extends ClassLoader {


    static {
        boolean result = ClassLoader.registerAsParallelCapable();
        if (!result) {
            system.out.println("pClassLoaderParallel.registrationFailed");
        }
    }
    ...
}

 

    实现原理并不复杂,不过如果你的程序是JDK 6升级迁移而来,那么还需要注意几个问题:

    1)如果你通过重写loadClass(String name)来实现类加载,那么你需要移除方法级别或者其内部的对象同步区块,同时还需要模仿JDK 7的方式,实现基于className级别的并发(可以参考loadClass实现);确保defineClass方法对于同一个className只能调用一次。

    2)原则上,重新findClass是不需要额外使用同步词修饰,如果有,你也应该移除。

    3)如果你的程序,在并发加载时有意外的兼容性问题,可以通过JVM参数来全局关闭此特性:-XX:+AlwaysLockClassLoader。

 

    在Tomcat 8之后,已经默认支持了并发加载。

 

    有没有什么特殊的场景,是不能使用“并发加载”的呢?这个问题就留给大家了!

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics