单例模式是23种设计模式中最简单、最常见的设计模式之一,许多人学习设计模式,第一个接触到的就是单例模式。
单例模式属于创建型模式,其目的是在当前进程中只创建一个实例,也可能是一个线程中属于单例。
想要写出安全又简洁的单例模式,如果不注意细节,很容易有潜在的bug,本文总结了最常见的4种单例模式写法,并分析每种优缺点。
懒汉模式
懒汉模式,就是在用的时候才会被创建,体现的是一种延迟加载的思想。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法lazy loading很明显,但是致命的是在多线程环境下不能正常工作,再来看看并发安全的懒汉模式。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是synchronized加锁之后,每次获取单例都需要上锁,所以效率很低,99%情况下不需要同步。再看下它的优化版:
public class Singleton {
private volatile static Singleton singleton;
private Singleton () {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这就是大名鼎鼎的双重检查锁方式,也有人质疑这种单例模式的可靠性,这里笔者不做过多讲解,有兴趣的伙伴可以去网上搜索:double-checked locking is broken。
需要注意的是,singleton实例变量必须加volatile关键字。加volatile,这里主要使用了它的有序性特性,可以禁止指令重排。有许多的博客作者说是利用了volatile的可见性,并不是的。
singleton = new Singleton();
是由三个步骤组成的:
-
为对象分配内存
-
实例化对象
-
将引用指向对应的内存地址
第2,3步可能发生指令重排列,第一个线程先将singleton指向一个未实例化对象的内存地址,然后再进行实例化对象。
若此时第二个线程进行第一个非空判断时,则为false,会直接返回还没有实例化对象的内存地址,从而可能产生空指针异常。
饿汉模式
饿汉模式就是在使用前,实力已经被创建了。其实现代码如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种方式基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化,显然没有达到lazy loading的效果,也有变种写法。
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton () {}
public static Singleton getInstance() {
return this.instance;
}
}
静态内部类
这是我在代码中比较常用的一种写法。
public class Singleton {
private static class SingletonHolder {
private SingletonHolder() {}
private static final Singleton INSTANCE = new Singleton();
public static final Singleton getInstance() {
return INSTANCE;
}
}
public static Singleton getSingleton() {
return SingletonHolder.getInstance();
}
}
或者:
public class Singleton2 {
private Singleton2() {}
private static final class Singleton2Holder {
public static final Singleton2 INSTANCE = new Singleton2();
}
public static Singleton2 getInstance() {
return Singleton2Holder.INSTANCE;
}
}
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,
这种方式与饿汉模式不同的是,是Singleton类被装载了,instance不一定被初始化,有lazy loading效果。
枚举
枚举和静态代码块的特性相似,使用枚举时,构造方法会被自动调用,也可以利用这个特性实现单例模式,不过比较少见。
public class EnumSingleton{
private EnumSingleton(){}
public static EnumSingleton getInstance(){
return Singleton.INSTANCE.getInstance();
}
private static enum Singleton{
INSTANCE;
private EnumSingleton singleton;
//JVM会保证此方法绝对只调用一次
private Singleton(){
singleton = new EnumSingleton();
}
public EnumSingleton getInstance(){
return singleton;
}
}
}
public static void main(String[] args) {
EnumSingleton obj1 = EnumSingleton.getInstance();
EnumSingleton obj2 = EnumSingleton.getInstance();
System.out.println("obj1==obj2?" + (obj1==obj2)); // true
}
总结:
单例模式主要有懒汉模式、饿汉模式、内部类、基于枚举,4种写法,基于内部类的方式比较常见,也是比较推荐的方式。
设计模式单纯的使用比较简单,在许多复杂的业务场景,常常会把多种模式混合起来使用,比如单例模式+工厂模式。而且重要的是融会贯通,不在于死记硬背,这样才能灵活多变的应用。
END