本文对设计模式中的单例模式进行总结。
饿汉式
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中大致进行了以下操作:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 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有两层语义:
- voaltile 变量是在主存中的,而不是线程的私有工作内存中,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
- 局部禁止指令重排。 但是需要注意在 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)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。