转载

懒加载的艺术

懒加载是一种加载方式,加载对象一般有两种方式,一是在启动时就立即创建好,另一种则是在需要用到的时候再去加载即懒加载。懒加载一般会针对单例场景,且一般是针对在加载消耗较大费时,且不一定会用到的场景。

好了,相信啥意思大家都明白!那么具体如何实现呢?其实挺有意思的!

方案1. 直接用懒加载实例进行判断null,不安全的做法

public class UnsafeLazyInit{ 
    private static Instance instance; 
     
    public static Instance getInstance(){ 
        if (instance == null){ 
            instance = new Instance(); 
         } 
         return instance; 
    } 
}  

很明显,这里的懒加载是不是安全的,因为当线程并发访问 getInstance(), 可能同时看到为null,从而同时进行了初始化,这会导致外部获取到的实例是不致的,从而导致不可预估的错误!另一种隐形的加载错误待说!

方案2. 使用锁将懒加载方法锁起来,不省力的做法

如上,既然既然访问 getInstance() 是非线程安全的,那么,只要加个锁就可以了!

    public static syncronized Instance getInstance(){ 
        if (instance == null){ 
            instance = new Instance(); 
         } 
         return instance; 
    } 

很明显,这种做法在高并发的情况下,会有严重的锁竞争,从而导致严重的性能问题!相信不会有同学这么干!

方案3. 双重锁校验的普通方法,一个有隐患的做法

如上两个问题,既要线程安全,又要性能不影响,其实可以想像到,初始化动作只是一次性的,所以,只要第一次的时候保证线程安全即可,因为后续大家都是获取同一个实例!所以,我们把锁的位置放到第一次加载时!

    public static Instance getInstance(){ 
        if (instance == null){ 
            // 只有在instance为null即未进行过初始化时,才会上锁,从而避免后续性能问题
            synchronized(UnsafeLazyInit.class){ 
                // 由于外面的判定是非线程安全的,上锁后,再次进行判定是否已创建
                if (instance == null){ 
                    // 此处初始化可能出现重排序
                    instance = new Instance(); 
                } 
            } 
        } 
        return instance; 
    } 

所以,如上的解决办法,看起来很完美。但其实还是有问题的!问题在于,外部的 instance == null 初始是非线程安全的,任何进入的线程都可以进行断定!

而 instance = new Instance(); 语句,并不是像代码看起来那样,就一句,可以保证原子性!

这一条语句在实际执行中,可能会被拆分程三条语句,即分配内存/初始化类变量/赋值给实例变量,大致如下:

    memory = allocate(); 
    ctorInstance(memory);
    instance = memory;

由于jvm的jit编译优化,可能会重排序,在保证结果最终一致的前提下,会将分配内存和赋值实例变量做不确定的重排,而当发生重排后,即先赋值实例变量内存空间,那么由于外部非线程安全的获取实例变量,会立即读取到该变量不为null,从而得到一个未初始化完成的实例进行后续操作!这将带来不可预知的后果!所以,这种双重锁是有问题的!不过看起来问题范围已经很小了!

方案4. 双重锁校验的增强方法,完美

综上双重锁方法,还存在一个重排序问题,一般针对重排序,我们要条件反射式的想到禁止重排序即可。而jvm禁止重排的方式,有 volatile, final, 等关键词,当然其实现原理都是加入一些内存屏障来保障不重排。不管怎么样,我们只需要使用一些关键词就可以了!

    private volatile static Instance instance;
    public static Instance getInstance(){ 
        if (instance == null){ 
            // 只有在instance为null即未进行过初始化时,才会上锁,从而避免后续性能问题
            synchronized(UnsafeLazyInit.class){ 
                // 由于外面的判定是非线程安全的,上锁后,再次进行判定是否已创建
                if (instance == null){ 
                    // 使用volatile后,禁止了jit重排优化
                    instance = new Instance(); 
                } 
            } 
        } 
        return instance; 
    } 

方案5. 使用类初始化机制创建对象,一种脆弱的加载方式

这是一种基于类初始化锁的一种懒加载方式!将懒加载放在一个类的静态变量上,依赖于类的安全的类加载来保证期实例化的线程安全性和准确性!如下:

public class InstanceFactory {
    private static class InstanceHolder {
        // 懒加载实例化放到内部类的静态变量上,需确定两个问题,1. 初始化时机,2. 线程安全性
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance;
    }
}

这看起来虽然有点麻烦,但是理解起来不会有问题!但是有问题我们得考虑下:静态变量不是一开始就会加载出来吗?如果这样的话,就不存在懒加载了啊!

其实static静态变量是在类初始化的时候才会操作的。

而类的初始化则有几个时机:

        1. 类首次被创建实例,即 new xxx() 操作时,触发类初始化;
        2. 类中的静态方法被首次调用,比如上面的 getInstance() 被首次调用时会触发当前类的初始化;
        3. 类的静态字段被赋值,比如 A.instance = abc;
        4. 类中的一个非常量字段被使用,常量则不会触发初始化;
        5. 类一个顶级类,而且一个断言语句嵌套在类内部执行;(我也不太明白啥意思)

来看一下实际的例子,说明类的初始化时机:

    @Test
    public void testClassInit() {
        // new 就不多说了
        // 静态方法被使用
        A.getInstance();
        // 常量使用不会触发类初始化
        System.out.println(A.noneInitConst);
        // 静态变量被赋值触发类初始化
        A.setVarInit = "";
        System.out.println("setVarInit=''");
        // 非常量静态变量被使用触发类初始化
        System.out.println(A.usedVarInit);
    }
    static class A {
        // 类常量被使用,不会触发类初始化
        public static final String noneInitConst = "a";
        // 静态变量赋值
        static String setVarInit;
        // 静态变量使用,触发类初始化
        static String usedVarInit = "c";
        static {
            // 类初始化时会执行该静态块
            System.out.println("A executed...");
        }
        // 静态方法触发类初始化
        public static String getInstance() {
            // 为避免其他规则被触发,直接使用返回字符串
            return "A";
        }
    }

可以依次注释各规则,查看初始化效果!

ok,明白了类初始化的时机后,我们知道了,这里的懒加载是有用的!

但是还有个问题,就是类初始化难道不会并发吗?答案是一定的,既然执行时机一致,那么并发自然存在。

类初始化时,jvm会去获取一个锁,从而保证同步多个线程对同一个类的初始化!这个从jdk的ClassLoader实现可以看出来!

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 获取锁
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

所以,类的初始化是线程安全的!从而得出,使用类的 static 变量进行懒加载的正确性!

当然,这里无故又多了一个内部类,着实让人不爽,而且如果后面往这个类里加入其他变量,则可能一不小心导致了问题!

不管怎么样,他能解决当下的问题!

方案6. 使用第三方变量来标识懒加载情况

意思是说,这里的初始化,是一个大对象的初始化,那么我可以换一个 true | false, 的简单变量的判定来处理,变量虽简单,不过也可能遇到的问题其实和上面一样,就不赘述了! 具体做法就是,在完成加载之后,再将该第变量值改变即可!

但是使用第三方变量还有个好处,就是可以很方便的执行代码块!复杂的加载逻辑,你懂的!

综上,懒加载这个简单操作,还真是充满了艺术感呢!

参考: 《java并发编程的艺术》

原文  http://www.cnblogs.com/yougewe/p/10090535.html
正文到此结束
Loading...