1
什么是单例模式
单例模式(Singleton pattern),在任何情况下,保证只有一个实例,并提供一个全局访问点,属于创建型设计模式。2
单例模式用途
在Java中,单例模式可以保证在一个JVM中只存在单一实例。单例模式有如下常用的场景:
- 一些需要频繁创建的类,单例化减少系统压力;
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用;
- 对资源进行频繁操作的对象,如线程池、缓存、数据库连接池和文件读取类等。
3
单例模式类图
这里以饿汉式为例给出的类图,下面将详细给出代码。
4
单例模式的几种实现方法
Singleton单例类的构造函数是private的,这是为了禁止外部代码调用构造函数创建实例。构造函数私有化后,我们一般会提供public的getInstance方法,用于获取实例。单例模式的常见实现,这里我总结如下:
下面我将依次实现,并尽可能说明每种方式的优劣。
饿汉式——经典写法
/** * 饿汉模式经典写法,简单实用,较为通用 */public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; }}
饿汉式——静态代码块写法
/** * 饿汉式的静态代码块写法 */public class HungryStaticSingleton { private static final HungryStaticSingleton instance; static { instance = new HungryStaticSingleton(); } private HungryStaticSingleton() { } public static HungryStaticSingleton getInstance() { return instance; }}
饿汉式总结:
- 优点:线程安全,执行效率高,简单易于实现,适用于单例对象较少的情况;
- 缺点:实例的初始化不会考虑系统是否需要,在类加载的时候就进行实例化。在系统存在较多单例对象时候,会带来内存资源浪费。
懒汉式单例
这里直接给出懒汉式的双重检查机制(DCL)的写法,其中的演化过程以及所解决的问题将简化描述,其详细的细节可查阅相关资料。/** * 这就是DCL双重检查机制,其是线程安全的 */public class LazyDoubleCheckSingleton3 { private volatile static LazyDoubleCheckSingleton3 instance; private LazyDoubleCheckSingleton3(){} public static LazyDoubleCheckSingleton3 getInstance() { // 第一个判断是否要等待获取锁,如果对象已经存在,则直接返回,大大提高了因获取锁带来的性能消耗 if (instance == null) { synchronized (LazyDoubleCheckSingleton3.class) { // 检查是否要重新创建实例 if (instance == null) { instance = new LazyDoubleCheckSingleton3(); } } } return instance; }}
关于上述双重检查机制的单例写法有以下几点说明:
- synchronized 同步代码块尽可能缩小同步范围,提高性能;
- 第一个判断:检查是否要等待获取锁,如果对象已经存在,则直接返回,大大提高了因获取锁带来的性能消耗;
- 第二个判断:检查是否要重新创建实例,防止多个线程并发创建;
- volatile 关键字解决new LazyDoubleCheckSingleton3()过程中的指令重排,防止实例instance获取到未初始化的对象;
- 基于volatile的解决方案需要JDK5或者更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)。
上述双重检查机制的volatile的补充说明(选读):
其代码中创建实例的部分:instance = new LazyDoubleCheckSingleton3();,这一步可以分解为如下三步来执行:
memory = allocate(); // 1: 分配对象的内存空间
ctorInstance(memory); // 2: 初始化对象
instance = memory; // 3: 设置instance指向刚分配的内存地址
上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上)。重排序后的伪代码可能是这样的:memory = allocate(); // 1: 分配对象的内存空间
instance = memory; // 3: 设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化 ctorInstance(memory); // 2: 初始化对象
重排序以后,就会有其他线程可能获取到未初始化完成的实例,带来风险。
静态内部类实现单例
JVM在类的初始化阶段,会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现静态内部类的单例。
/** * 静态内部类实现单例 * 这种写法由JVM的类加载机制保证实例的线程安全,以及延迟加载的特性;而且避免了synchronized带来的性能消耗 */public class LazyStaticInnerClassSingleton { private LazyStaticInnerClassSingleton(){} public static LazyStaticInnerClassSingleton getInstance() { return LazySingletonHolder.instance; } private static class LazySingletonHolder { private static final LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton(); }}
静态内部类写法的总结:
- 优点:延迟加载,内部类只有在使用到该实例的时候才会加载;避免了synchronized带来的性能问题;
- 缺点:不能解决反射和序列化的破坏,这也是饿汉式和懒汉式写法的问题。
枚举单例
枚举单例是一种较为完备的方案,其可以解决反射和序列化对单例的破坏。
public enum EnumSingleton { INSTANCE; // 枚举的属性 private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; }}
在稍后的文章中,将详细叙说下枚举单例的应用。
5
总结
在实际开发中,我们一般推荐饿汉式的经典写法,静态内部类的写法。关于单列模式的多种写法以及测试案例,可以在如下地址找到:
https://github.com/SmilingBug/design-patterns-java
7
参考引用
[1] 谭勇德,设计模式就该这么学,中国工信出版社
[2] 【日】结城浩,图解设计模式
[3] 高洪岩,Java多线程编程核心技术,机械工业出版社
[4] 方腾飞、魏鹏、程晓明,Java并发编程的艺术
[5] JosHua Bloch,Effective Java