单例模式
介绍
单例模式(Singleton Pattern)可能是设计模式中用的最多的模式,单例模式非常简单同时代码也比较短方便手写代码,所以也是面试中经常会问到的设计模式。单例模式它是一种对象创建模式,它用于确保系统中一个类只产生一个实例。例如:一个系统使用AppConfig对象读取诸如xml和properties配置文件,而当系统运行时,能够存在多个AppConfig对象,而每一个AppConfig对象中都封装着配置文件,这样非常浪费内存资源,因此系统内部只需一个该配置对象,这里就需要使用单例模式了。
本文针结合网上的资料对单例模式的常有的写法进行整理,希望对你有一定帮助。
最直接的单例模式
当面试题被问到这一题的时候,很多人的根据直觉可能写出下面的代码。
public class Singleton01 { private static Singleton01 instance; private Singleton01() {} public static Singleton01 getInstance() { if (instance == null) { instance = new Singleton01(); } return instance; } }
上面的代码简单明了,是很多人的最终答案,上面的代码还使用懒加载模式(就是需要使用实例对象时,才创建对象)。但有个明显的缺陷,就是只能在单线程中使用,在多线程时,会创建多个实例,造成内存泄漏。
线程安全的单例模式
遇到多线程的问题,自然而然地想到加锁,对获取实例方法加锁后,多线程情况下就可以保证线程安全。
public class Singleton02 { private static Singleton02 instance; private Singleton02() { } public static synchronized Singleton02 getInstance() { if (instance == null) { instance = new Singleton02(); } return instance; } }
但上面的代码,虽然是线程安全,但是每一次使用getInstance()获取实例时,都必须加同步锁,加锁是很耗时的操作,在没有必要的时候我们应该尽量避免。
双重检验锁
对于上面的效率不高的问题,我们可以使用双重检验锁(Double-checked locking)来进行解决,我们应该在实例还没有创建之前需要加锁操作,以保证只有一个实例,当实例创建之后,就不再需要做加锁操作,在此过程中,会两次检查instance==null,称为double checked,其中一次在同步块外,一次在同步块内,至于为什么在同步块内还要检验一次,是因为可能多个线程都进行if代码中,如不进行检验,就可能创建多个实例。
public class Singleton03 { private static Singleton03 instance; private Singleton03() { } public static synchronized Singleton03 getInstance() { if (instance == null) { // Single check synchronized (Singleton03.class) { if (instance == null) { // double check instance = new Singleton03(); } } } return instance; } }
上面的代码看上去非常完美,但是还存在问题,主要在于instance = new Singleton03();这一句,这个语句并不是一个原子操作,它可以分解为下面三步:
1. 给对象引用instance分配内存 ;
2. 调用Singleton03的构造函数来初始化对象成员变量;
3. 将生成的实例对象的内存地址赋值给对象引用instance。
然而上面的3个步骤并不是最终的执行顺序,JVM中即时编译器中存在指令重排序的优化,最后的执行顺序,可能是1-2-3,也可能是1-3-2,当为第二种情况时,线程一刚执行第3步,线程二直接返回instance,然后instance这个对象引用已经指向堆内存,但堆内存中却没有初始化,于是报错,对于这个问题将instance声明为volatile即可。
public class Singleton03 { private volatile static Singleton03 instance; private Singleton03() { } public static synchronized Singleton03 getInstance() { if (instance == null) { // Single check synchronized (Singleton03.class) { if (instance == null) { // double check instance = new Singleton03(); } } } return instance; } }
饿汉式
上面的单例模式都是使用懒加载的方式,可以称为懒汉式,这里介绍的是饿汉式,顾名思义,实例对象在使用之间就创建好了,直接用就可以了。
public class Singleton04 { private final static Singleton04 INSTANCE = new Singleton04(); private Singleton04() { } public static Singleton04 getInstance() { return INSTANCE; } }
上面的代码中当classloader加载Singleton04类时就创建了该实例,之后可以直接使用,这样一定是线程安全的,但这样的话,我们将实例的创建委托给类加载器,可能与我们要求的行为可能不太一致,我们可能希望在使用getInstance方法才创建单例对象。
静态内部类
为了解决上面的问题,我们可以使用静态内部类的方法,既可以在第一次使用getInstance时创建单例对象,又可以保证同步安全。
public class Singleton05 { private static class SingletonHolder { private final static Singleton05 INSTANCE = new Singleton05(); } private Singleton05() { } public static Singleton05 getInstance() { return SingletonHolder.INSTANCE; } }
上面的代码,SingletonHolder为私有静态类,只能通过getInstance访问,所以是懒加载的,同时获取实例时无需同步,没有性能上的损失。
枚举
最简单的方式居然是用枚举,让人意想不到。
public enum Singleton06 { INSTANCE; private Singleton06() { } public static Singleton06 getInstance() { return INSTANCE; } }
使用枚举创建实例默认是线程安全的,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。上面的getInstance方法可以不要,可以直接使用Singleton06.INSTANCE进行使用。
总结
单例模式的特点:
1、类的实例对象只有一个;
2、可以通过类自身的方法创建实例对象 ;
3、系统使用的对象为同一个对象。
单例模式的使用方法:小小的单例模式同样有很多种不同的写法,但根据我的经验推荐大家使用双重检验锁的方式,或直接使用饿汉式的方式,使用枚举的方式实际工作中感觉很少使用。
单例模式的注意事项:单例模式它是有范围的,它只局限于类加载器(ClassLoader)中能保证实例唯一,当有不同的类加载器时,会出现不同的类加载器装载同一个类,从而产生多个实例,所以,应该尽量保证多个类加载器不会装载同一个单例类。