【设计模式】单例模式的几种写法

468 阅读4分钟

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

饿汉式单例

在类初始化时就已经自行实例化了

      public class Singleton {
          //私有静态成员变量
          private static Singleton instance = new Singleton();
          //私有构造方法
          private Singleton(){}
          //公有静态访问方法
          public static Singleton getInstance()
          {
              return instance;
          }
      }

这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。

懒汉式单例

在第一次调用实例的时候才实例化

public class Singleton {
      //私有静态成员变量
      private static Singleton instance = null;
      //私有构造方法
      private Singleton(){}
      //公有静态访问方法,注意加了一个synchronized关键字确保线程安全
      public static synchronized Singleton getInstance()
      {
          if(instance == null)
              instance = new Singleton();
          return instance;
      }
  }

这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。

双重检查锁

public class Singleton {
    private static Singleton sInstance;
    public static Singleton getInstance() {
        if (sInstance == null) {  // 位置1
            synchronized (Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();  // 位置2
                }
            }
        }
        return sInstance;
    }
    private Singleton() {}
}

这种方式使用双重检查锁,多线程环境下执行getInstance()时先判断单例对象是否已经初始化,如果已经初始化,就直接返回单例对象,如果未初始化,就在同步代码块中先进行初始化,然后返回,效率很高。

但是这种方式是一个错误的优化,问题的根源出在位置2

sInstance =new Singleton();这句话创建了一个对象,他可以分解成为如下3行代码:

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

上述伪代码中的2和3之间可能会发生重排序,重排序后的执行顺序如下:

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

因为这种重排序并不影响Java规范中的规范:intra-thread sematics允许那些在单线程内不会改变单线程程序执行结果的重排序。

但是多线程并发时可能会出现以下情况: 线程B访问到的是一个还未初始化的对象。

解决方案1:

public class Singleton {
private static volatile Singleton sInstance;
public static Singleton getInstance() {
        if (sInstance == null) {
                synchronized (Singleton.class) {
                        if (sInstance == null) {
                           sInstance = new Singleton();
                            }
                        }
                    }
                    return sInstance;
            }
private Singleton() {}
}

将对象声明为volatitle后,前面的重排序在多线程环境中将会被禁止

解决方案2:

public class Singleton {
    private Singleton(){};
    private static class Inner{
        private static Singleton SINGLETION=new Singleton();
    }
    public static Singleton getInstance(){
        return Inner.SINGLETION;
    }
}   

静态内部类不会随着外部类的初始化而初始化,他是要单独去加载和初始化的,当第一次执行getInstance方法时,Inner类会被初始化。

静态对象SINGLETION的初始化在Inner类初始化阶段进行,类初始化阶段即虚拟机执行类构造器<clinit>()方法的过程。

虚拟机会保证一个类的<clinit>()方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的<clinit>()方法,其它线程都会阻塞等待。

枚举

public enum Singleton {  
     INSTANCE;  
     public void doSomeThing() {  
     }  
 }  

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:

private Object readResolve() throws ObjectStreamException {
	return singleton;
}

参考文章