单例模式是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



