zoukankan      html  css  js  c++  java
  • 静态工厂方法中单例的延迟加载技术

      在用Java实际开发的时候我们经常要用到单例模式,一般来说,单例类的实例只能通过静态工厂方法来创建,我们也许会这样写:

    public final class Singleton  
    {  
        private static Singleton singObj = new Singleton();  
      
        private Singleton(){  
        }  
      
        public static Singleton getSingleInstance(){  
           return singObj;
        }  
    }  

      这种方法可能会带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。因此我们希望在用到它的时候再加载它,这种设计思想就是延迟加载(Lazy-load Singleton)。

    public final class LazySingleton  
    {  
        private static LazySingleton singObj = null;  
      
        private LazySingleton(){  
        }  
      
        public static LazySingleton getSingleInstance(){  
            if(null == singObj )    //A线程执行
           singObj = new LazySingleton(); //B线程执行 return singObj; } }

      上面的写法就保证了在对象使用之前是不会被初始化的。这种方式对于单线程来说是没什么问题,但是在多线程的环境下却是不安全的。假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。如何避免这个问题,答案很简单,在那个方法前面加一个Synchronized就OK了。

    public final class ThreadSafeSingleton  
    {  
        private static ThreadSafeSingleton singObj = null;  
      
        private ThreadSafeSingleton(){  
        }  
      
        public static Synchronized ThreadSafeSingleton getSingleInstance(){  
            if(null == singObj ) singObj = new ThreadSafeSingleton();
                return singObj;
        }  
    }  

      但是众所周知,同步对性能是有影响的。由于对getInstance()做了同步处理,synchronized将导致性能开销。如果getInstance()被多个线程频繁的调用,将会导致程序执行性能的下降。那么有没有什么方法,一方面是线程安全的,有可以有很高的并发度呢?我们的能想到的对策是降低同步的粒度,因此,人们想出了一个“聪明”的技巧:双重检查锁定(double-checked locking)。

    public final class DoubleCheckedSingleton  
    {  
        private static DoubleCheckedSingletonsingObj = null;  
      
        private DoubleCheckedSingleton(){  
        }  
      
        public static DoubleCheckedSingleton getSingleInstance(){  
            if(null == singObj ) {                                //第一次检查
                  Synchronized(DoubleCheckedSingleton.class){     //加锁
                         if(null == singObj)                      //第二次检查
                               singObj = new DoubleCheckedSingleton();
                  }
             }
           return singObj;
        }  
    }  

      如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。这看起来似乎两全其美,但是这是一个错误的优化!原因在于singObj = new DoubleCheckedSingleton()这一句。

      以上有问题的这一行代码可以分解为如下的三行伪代码:

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

      在一些编译器中会对代码进行优化而对语句进行重排序,这很常见,对于上面的三行伪代码,2和3可能会被重排,结果如下:

    memory = allocate();   //1:分配对象的内存空间
    instance = memory;     //3:设置instance指向刚分配的内存地址
                           //注意,此时对象还没有被初始化!
    ctorInstance(memory);  //2:初始化对象

      所以在多线程的环境下线程A可能执行到3,这时候对象在内存中已经有地址了,但是还未被完全初始化,这时候一旦线程B执行,在第一次检查的时候(null == singObj)返回false,线程B以为对象已经存在,接下来就可以使用了。实际上线程B可能使用了一个没有被完全初始化的对象,运行结果不得而知。

       对于这种问题解决方法有两种,“基于volatile的双重检查锁定的解决方案”和“基于类初始化的解决方案”,在双重检查锁定与延迟初始化这篇文章中有详细介绍。我们来说下第二种解决方案。这种解决方案也就是Initialization On Demand Holder idiom,这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。

    public class Singleton    
    {    
        private static class SingletonHolder    
        {    
            public final static Singleton instance = new Singleton();    
        }    
       
        public static Singleton getInstance()    
        {    
            return SingletonHolder.instance;    
        }    
    }

      内部类的初始化是延迟的,外部类初始化时不会初始化内部类,只有在使用的时候才会初始化内部类。而Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。也就是说,SingletonHolder在各个线程初始化的时候是同步执行的,且全权由JVM承包了。

      Initialization On Demand Holder idiom的实现探讨中分析了单例的几种描述符(private static final / public static final / static final)之间的合理性,其推荐使用最后一种描述符方式更为合理。

    参考资料:

    http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization

    http://ifeve.com/initialization-on-demand-holder-idiom/ 

    http://stackoverflow.com/questions/20995036/is-initialization-on-demand-holder-idiom-thread-safe-without-a-final-modifier

    http://stackoverflow.com/questions/21604243/correct-implementation-of-initialization-on-demand-holder-idiom

    http://blog.sina.com.cn/s/blog_75247c770100yxpb.html

  • 相关阅读:
    jenkins常用插件汇总
    Jenkins
    如何在Visual Studio中配置git
    IP地址分类/IP地址10开头和172开头和192开头的区别/判断是否同一网段(A、B、C三类地址)【转】
    k8s 之如何从集群外部访问内部服务的三种方法
    k8s使用外部mysql做内部服务
    oracle判断某个字符串是否包含某个字符串的三种方法
    Python多进程共享numpy 数组
    【转载】 源码分析multiprocessing的Value Array共享内存原理
    集成SWAGGER2服务-spring cloud 入门教程
  • 原文地址:https://www.cnblogs.com/big-xuyue/p/4074645.html
Copyright © 2011-2022 走看看