美文网首页
Java对象延迟初始化的实现

Java对象延迟初始化的实现

作者: pedro7 | 来源:发表于2021-01-16 11:42 被阅读0次

    一、什么是延迟初始化?

    在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。
    延迟初始化实际上就是:当我们要进行一些高开销的对象初始化操作时,只有在使用这些对象时才进行初始化。最显著的意义在于,假如程序实际上不会用到这些类,那初始化它们的开销就会被完全避免。

    二、延迟初始化的错误实现方式

    1、线程不安全的延迟初始化

    public class UnsafeLazyInitialization {
        private static Instance instance;
    
        public static Instance getInstance() {
            if (instance == null) //1:A线程执行
                instance = new Instance(); //2:B线程执行
            return instance;
        }
    
        static class Instance {
        }
    }
    

    在UnsafeLazyInitialization类中,假设线程A执行代码1的同时,B线程执行代码2。此时线程A很可能看到instance引用的对象还没有完成初始化。所以我们必须对1、2这两步操作进行同步处理。

    2、直接使用synchronized进行同步-有巨大的性能开销

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

    在早期的JVM中,使用synchronized存在巨大的性能开销,这在实际的应用中时几乎不可能被接受的。

    3、双重检查锁定-看似聪明的解决方案

    public class DoubleCheckedLocking { //1
        private static Instance instance; //2
    
        public static Instance getInstance() { //3
            if (instance == null) { //4:第一次检查
                synchronized (DoubleCheckedLocking.class) { //5:加锁
                    if (instance == null) //6:第二次检查
                        instance = new Instance(); //7:问题的根源出在这里
                } //8
            } //9
            return instance; //10
        } //11
        static class Instance {
        }
    }
    

    为了克服同步带来的大量开销,人们想得到了双重检查锁定这一看似聪明的解决方案。目的是仅仅对一开始竞争状态的getInstance加锁,带来开销。

    但由于代码可能的重排序,直接使用上述代码是一种错误的优化。原因如下:
    示例代码第七行,即 instance = new Instance(); 可以分解为如下的三行伪代码

    memory = allocate();//1:分配对象的内存空间
    ctorInstance(memory);//2:初始化对象
    instance = memory;//3:设置instance指向刚分配的内存地址
    

    假如2、3之间发生重排序,可能顺序变成如下这样

    memory = allocate();//1:分配对象的内存空间
    instance = memory;//3:设置instance指向刚分配的内存地址
    ctorInstance(memory);//2:初始化对象
    

    也就是说,当instance已经指向分配的内存地址时,对象还没有被初始化。
    我们再回到示例代码

    public class DoubleCheckedLocking { //1
        private static Instance instance; //2
    
        public static Instance getInstance() { //3
            if (instance == null) { //4:第一次检查
                synchronized (DoubleCheckedLocking.class) { //5:加锁
                    if (instance == null) //6:第二次检查
                        instance = new Instance(); //7:问题的根源出在这里
                } //8
            } //9
            return instance; //10
        } //11
        static class Instance {
        }
    }
    

    当线程A在进行代码的第7行,即new Instance时,内部发生了重排序,即 instance = memory 在 ctorInstance(memory) 后进行。假如进程A刚刚进行到这两步之间。而进程B恰巧在第四行(第一次检查)处进行判断。那么线程B判断instance不为null,很可能向下进行,进而访问instance所引用的对象。而这时进程A尚未初始化instance!从而程序发生错误。

    三、延迟初始化的正确实现方式

    在上面的说明中了解了问题的根源后,我们可以很容易想到两个方法来实现线程安全的延迟初始化。
    (1)不允许伪代码中的2和3两行重排序
    (2)允许2和3重排序,但不允许其他线程"看到"这个重排序

    1、基于volatile的解决方案

    我们只要对之前双重检查锁定的代码进行一些小小的修改,就可以实现我们期望中的延迟初始化。

    public class SafeDoubleCheckedLocking {
        private volatile static Instance instance;
    
        public static Instance getInstance() {
            if (instance == null) {
                synchronized (SafeDoubleCheckedLocking.class) {
                    if (instance == null)
                        instance = new Instance();//instance为volatile
                }
            }
            return instance;
        }
    
        static class Instance {
        }
    }
    

    当instance的引用被声明为volatile时,创建对象时的重排序就将在多线程环境中被禁止。从而实现了用双重检查锁定来实现延迟初始化。

    注:这个方案需要JDK5以上版本,因为自JDK5开始使用新的JSR-133内存模型,这个规范增强了volatile的语义。

    2、基于类初始化锁的解决方案

    JVM在类的初始化阶段(即Class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会获取一个锁,这个锁可以同步多个线程对一个类初始化。基于这个特性,我们可以用以下的方式来实现延迟初始化。
    注意这个锁是对于类的初始化,而非对象的!

    public class InstanceFactory {
        private static class InstanceHolder {//利用这个类的初始化锁
            public static Instance instance = new Instance();
        }
    
        public static Instance getInstance() {
            return InstanceHolder.instance; //这里将InstanceHolder被初始化
        }
        static class Instance {
        }
    }
    

    当getInstance第一次被调用,发生竞争时,InstanceHolder将被初始化。其中的静态变量instance也在此时被初始化。而InstanceHolder这个类的初始化锁保证了instance的初始化是被同步的。即无论new instance时是否发生重排序,都不会被其他线程所看到。从而解决了同步问题。

    类的初始化锁相关知识在此不赘述,可参考《The Art of Java Concurrency Programming》相关篇目。

    四、两种解决方案的对比

    我们很容易可以发现,基于类初始化锁的方案的实现代码要更加简洁。但基于volatile的双重检查锁定方案有一个额外的优势,它可以对实例字段实现延迟初始化。
    当我们进行延迟初始化处理时,面对实例字段我们使用基于volatile的方案,面对静态字段我们使用集于类初始化锁的方案。

    2021-1-16

    相关文章

      网友评论

          本文标题:Java对象延迟初始化的实现

          本文链接:https://www.haomeiwen.com/subject/rvbgaktx.html