zoukankan      html  css  js  c++  java
  • lazy-init 懒加载的艺术

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

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

    方案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并发编程的艺术》

  • 相关阅读:
    正则中的顺序环视和逆序环视
    LeetCode 第 27 场双周赛
    LeetCode 每日一题 198. 打家劫舍
    LeetCode 每日一题 974. 和可被 K 整除的子数组
    LeetCode 每日一题 287. 寻找重复数
    LeetCode 每日一题 4. 寻找两个正序数组的中位数
    LeetCode 每日一题 146. LRU缓存机制
    LeetCode 每日一题 105. 从前序与中序遍历序列构造二叉树
    [转]多线程的那点儿事
    LeetCode 每日一题 5. 最长回文子串
  • 原文地址:https://www.cnblogs.com/yougewe/p/10090535.html
Copyright © 2011-2022 走看看