单例模式实现延迟加载在多线程下的思考(双检锁和内部类)

本文参考了 《java并发编程艺术》一书中的部分内容,增加了部分自己的理解。

1、单例模式最简单的实现:
  1. class  Single{
  2.        public static Single single = new Single();
  3.        private Single(){}
  4.        public static getInstance(){
  5.               return single ;
  6.        }
  7. }
此方法的缺陷是不能实现延迟加载。

2、通过synchronized实现延迟加载
  1. class  LazySingle{
  2.        private static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static synchronized getInstance(){
  5.               if(single  == null ){
  6.                        single  = new LazySingle();
  7.               }
  8.               return single ;
  9.        }
  10. }
此方法存在性能问题,多线程调用getInstance()方法,都需要检查锁是否存在;


3、通过双检锁实现

  1. class  LazySingle{
  2.        private static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static  getInstance(){
  5.               if(single  == null ){
  6.                       synchronized(LazySingle.class){
  7.                             if(single  == null){
  8.                                  single  = new LazySingle(); 
  9.                             }
  10.                        }
  11.               }
  12.               return single ;
  13.        }
  14. }

此方法看起来貌似很完美,当single没有实例化走同步方法,当single已经实例化则不需要走同步方法,有效的解决了性能问题。 但是此方法是一个错误的优化。
当线程执行到第5行时,代码读取到single不为空时,single引用的对象可能还没有完成初始化。
我们对第8行代码进行分析(single = new  LazySingle()), 这行代码在编译器中会被翻译成如下伪代码:

  1. memory = allocate();   //分配对象的内存空间
  2. ctorInstance(memory); //初始化对象
  3. instance = memory;     //设置instance指向刚分配的内存地址
上面第2行和3行代码可能会被重排序(为什么会重排序,大家可以参考《java并发编程艺术》一书),重排序的结果为
  1. memory = allocate();   //分配对象的内存空间
  2. instance = memory;     //设置instance指向刚分配的内存地址
  3. ctorInstance(memory); //初始化对象
 此时,如果2行执行结束,刚好有个线程访问到双检索代码的第5行时,single并不为空,但是初始化还未完成,将会返回一个空的引用。 
解决这个问题的方法有两种。

4、通过volatile解决
  1. class  LazySingle{
  2.        private volatile static LazySingle single = null
  3.        private LazySingle(){}
  4.        public static  getInstance(){
  5.               if(single  == null ){
  6.                       synchronized(LazySingle.class){
  7.                             if(single  == null){
  8.                                  single  = new LazySingle(); 
  9.                             }
  10.                        }
  11.               }
  12.               return single ;
  13.        }
  14. }

5、内部类方式解决
JVM在执行类的初始化期间,会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
初始化一个类包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。
因此当在单例类中定义 public static Single single = new Single(); 的变量时,不具备延迟加载的特性,JVM在load到这个类时就就行了变量的初始化。

根据java语言规范,在首次发生如下任意情况时,一个类或者接口类型T将被立即初始化:
  •     T是一个类,而且一个T类型的实例被创建;
  •     T是一个类,且T中声明的一个静态方法被调用;
  •     T中声明的一个静态字段被赋值;
  •     T中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
  •     T是一个顶级类,而且一个断言语句嵌套在T内部被执行;
    
public class StaticSingle {
 private StaticSingle(){
 
 }
 private static class StaticSingleHolder{
      private static StaticSingle instance = new StaticSingle();
 }
 public static StaticSingle getInstance(){
      return StaticSingleHolder.instance;
 }
}

使用内部类的方式实现单例,既可以实现延迟加载,也不必使用同步关键字,是一种比较完善的实现


你可能感兴趣的:(设计模式,并发编程)