单例模式

一、引言:

   单例(Singleton)通常被用来代表那些本质上唯一的系统组件,在Java或是Android开发中有着广泛的应用,比如说建立一个数据API接口的管理类就可以采用单例模式,又或者是建立一个实现图片加载功能的类。简单来说,单例即保证一个类仅有一个实例,并可以提供一个访问它的全局访问点,从而使得在需要这个类的实例的时候可以获取到。

二、单例的几种简单实现

  考虑到单例的实际使用环境中一般存在一定的并发情况,从而我们除了考虑单例的性能之外,还需要着重考虑单例的线程安全性。

   1.饿汉模式(线程安全)

public  class Singleton{
    private static Singleton instance = new SingleTon();
    private Singleton(){
    }
    public static Singleton getInstance(){
        return instance;
    }
}

  不难发现这种方法其实主要就是利用了static关键字,在类加载过程中就完成了初始化,所以类加载速度会比较慢,但获取对象的速度快。

  2.懒汉模式(线程不安全)

public class Singleton{
    private static Singleton instance;
    private Singleton(){

    }
    public static Singleton getInstance(){
        if(instance ==null){
            instance =new Singleton();
        }
        return instance;
    }
}

   这种方法只是声明了一个静态对象,其初始化被延迟到用户第一次调用获取实例的时候,这种方法能够提高类的加载速度,但在第一次使用实例时需要实例化,并且在多线程环境下不能正常工作。

  3.懒汉模式(synchronized  线程安全)

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

  这种方法其实就是在不安全的懒汉模式上增加了synchronized关键字,从而实现实例化方法的同步,实现了线程的安全。但在每次调用时都需要进行同步,开销开大。

  4.双重检查模式(线程安全)

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

  在前面一种方法的基础上,双重检查顾名思义就是增加了一重检查,即对instance是否已经实例化进行了两次判断,此外,还可以发现这种方法和上一种方法还存在一个差别,即对Singleton对象的声明中增加了一个关键字volatile。具体原因如下:

当线程执行到instance = new Singleton()时,虽然看上去它只有一条语句,但实际上它并不是一个原子操作,最终会变编译为多条指令:
1.给Singleton的实例分配内存
2.调用Singleton()的构造函数,初始化成员变量。
3.将instance对象指向分配的内存空间(这个时候instance才不是null)
现在估计你已经明白了,因为Java编译器的重排序优化以及Java内存模型中对Cache、寄存器到主内存的回写顺序的规定,上述2、3两步的顺序是无法确定的。这样就有可能导致错误。
而加入了volatile关键字后,即强调instance对象每次都是从主内存中读取,即可以实现安全的单例模式。
  相比前面几种实现,这种方法在性能上有所提高,并且是线程安全的。

 5.静态内部类模式

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

这边采用了静态内部类的一个特性,在第一次加载外部类的时候不会加载静态内部类,当我们第一次调用getInstance方法企图获取instance实例的时候内部类才会初始化。因此这种方式不仅能够保证线程的安全,也能保证单例对象的唯一性,同时延迟了单例的初始化,是这里推荐的一种单例的实现方法。

6.枚举单例

public enum Singleton{
    INSTANCE;
}

枚举单例的创建是线程安全的,并且在任何情况下都是单例。

嗯?难道前面几种方法在一定情况下不会是单例的吗?对,前面几种方法在一种情况下会出现重新创建对象的情况,这种情况就是序列化后再反序列化的时候。

当我们把一个单例的实例对象以序列化的方法写到磁盘再反序列化读回来后从而有效的获得了一个实例。

为了杜绝这种情况,反序列化操作中有一个私有方法readResolve()方法,这个方法可以控制对象的反序列化,我们需要在反序列化时加入如下方法,

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

7.容器单例模式

  在介绍完上述比较常规的方法后,我们来了解一种不一样一点的实现方法,采用Java容器来实现单例。

public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

在这里,我们实现了一个HashMap,在程序的一开始,我们可以根据实际需要,将多种单例类型注入到一个这个管理类中,交由HashMap进行保管,在使用时,只需要根据key就可以获取到对应类型的对象,这种方式可以让我们管理多种类型的单例,并且在使用时通过统一的接口进行获取操作。

三、总结

  看了这么多单例的实现方法,其实他们的基本思想就是将构造函数私有化,并且通过静态方法来获取一个唯一的实例(枚举除外),我们的实现方法需要保证线程安全,并且防止因为反序列化导致重新生成实例对象等问题(在Effective Java中有提到)。

原文地址:https://www.cnblogs.com/hustzhb/p/7096327.html