设计模式学习(十一)——单例模式

179 阅读6分钟

单例模式的定义与特点

单例(Singleton)模式的定义:指一个类只有一个实例,且这类能自行创建这个实例的一种模式。例如,Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致错误。


单例模式有3个特点

  1. 单例类只有一个实例对象
  2. 该单例对象必须由单例类自行创建
  3. 单例类对外提供一个访问该单例的全局访问点。


单例模式的结构与实现

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new构造函数”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。


1. 单例模式的结构 

单例模式的主要角色如下。 

  • 单例类:包含一个实例且能自行创建这个实例的类。
  •  访问类:使用单例的类。 

其结构如图 1 所示。



一、单例模式

1、定义

作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类


2、特点

单例类只有一个实例

单例类必须自己创建自己的唯一实例

单例类必须给索引其他对象提供这个实例


二、创建单例模式的方式

1、懒汉式,线程不安全

懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不会着急。会一直等到马上要使用对象才会实例化。

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

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

这段代码简单明了,而且使用了懒加载模式,但是存在致命的问题。当多个线程并行调用getInstance()的时候,就会创建多个实例,即在多线程下不能正常工作。


2、懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个getInstance()方法设为同步(synchronized)

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

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用getInstance()方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。


3、双重检验锁

双重检验锁模式,是一种使用同步块加锁的方法。程序员称其为双重检验锁,因为会有两次检查instance== null,一次是在同步块外,一次是在同步块内。

为什么在同步块内还要再检验一次?因为可能多个线程一起进入同步块外的if,如果在同步块内不进行二次检验的话就会生成对个实例

public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

但是这段代码看起来很完美,很可惜,它存在问题。主要在于instance = new Singleton()这句,它并非一个原子操作,实际上在JVM中这句话大概做了三个步骤

  1. 给instance分配内存
  2. 调用Singleton的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步instance就非null)

但是在JVM的即时编译器存在指令重排序的优化。因而上述第二步和第三步的顺序是不能得到保证的,最终的执行顺序可能是1-2-3也可能是1-3-2,。如果是后者,则在3执行完毕,2未执行前,被线程二抢占,这时instance已经非null(但还没有初始化),所以线程二会直接返回instance,然后使用,此时会报错。


因而需要将instance变量声明为volatile,就可禁止指令重排序。

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

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


4、饿汉式,static final field

饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,于是在装载类的时候就创建对象实例。


这种方法非常简单,因为单例的实例被声明成static和final变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

缺点是它不是一种懒加载模式,单例会在加载类后一开始就被初始化,及时客户端没有调用getInstance()方法。

饿汉式的创建方式在一些场景中无法使用,如Singleton实例的创建时依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那么这种单例写法就无法使用了。


5、静态内部类 static nested class

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

这种写法仍然使用JVM本身机制保证了线程安全问题。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类SingletonHolder,在该内部类中定义了一个static类,的变量INSTANCE,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只会初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。


由于SingletonHolder是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。


6、枚举

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是生命枚举实例的通常做法。

public enum EasySingleton{
    INSTANCE;
}

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。