单例模式

360 阅读4分钟

本文对设计模式中的单例模式进行总结。

饿汉式

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

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

这种方式非常简单,由于实例被声明为static和final的,因此在类第一次加载到内存时就会初始化,所以创建实例的过程是线程安全的。但是这个方式并非懒加载,比较消耗内存资源。 另外,饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

懒汉式,线程不安全(非正确写法)

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

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

这段代码采用了懒加载模式,在单线程中可以满足要求,但是当多线程调用getInstance()时,会出现线程安全问题,即可能创建多个实例。

懒汉式,线程安全

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

最简单的解决方法是将上面的整个getInstance()方法设为同步,这样在多线程场景也可以满足要求,但是每个线程每次运行到此方法都会尝试获取锁,比较消耗性能,实际上只有第一次创建实例时的同步是有意义的,这就引出了双重检测锁。

双重检验锁 优化性能

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

双重检测锁不再同步整个方法,转而同步代码块,并且在同步代码块的外侧加了一层检测,避免了无意义的性能消耗。 然而这段代码还是有问题的,问题由同步块外侧的检测if (instance == null)和内侧实例化语句instance = new Singleton()共同造成。因为实例化过程并非一个原子操作,实际上在JVM中大致进行了以下操作:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 但是因为在Jvm层面和cpu层面都存在指令重排的优化,上述操作在并发情况下并不一定是顺序执行的,也就是说,可能出现1-3-2的顺序,将未完成初始化的 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;
    }
   
}

volatile有两层语义:

  1. voaltile 变量是在主存中的,而不是线程的私有工作内存中,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
  2. 局部禁止指令重排。 但是需要注意在 Java 5 之前的版本中,JVM的设计存在缺陷,即使 volatile 变量也无法避免指令重排,不过这个问题在 Java 5之后已经解决了。

静态内部类

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本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

枚举

public enum EasySingleton{
    INSTANCE;
}

枚举应该是最简洁的方法了,我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。不过这种方式也并非懒加载,其实例在枚举类被加载的时候初始化。

总结

一般来说,单例模式有五种写法:饿汉、懒汉、双重检验锁、静态内部类、枚举。

一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。