单例设计模式
1、定义
单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
单例模式的三个要点:
- 只有一个实例
- 这个类必须自行创建这个实例
- 这个类必须自行向整个系统提供这个实例
2、单例模式的七种实现方式
2.1 懒汉模式
单例实例在第一次使用的时候被创建
这种方式是线程不安全的,多个线程同时调用,可能会创建多个对象
示例代码
public class SingletonDemo1 {
private SingletonDemo1() {}
private static SingletonDemo1 instance = null;
public static SingletonDemo1 getInstance() {
if (instance == null) {
instance = new SingletonDemo1();
}
return instance;
}
}
2.2 饿汉模式
在类装载的时候被创建,是实现起来最简单的单例类
这种方式是线程安全的
示例代码
public class SingletonDemo2 {
private SingletonDemo2() {}
private static SingletonDemo2 instance = new SingletonDemo2();
public static SingletonDemo2 getInstance() {
return instance;
}
}
以上两种实现方式是懒汉模式与饿汉模式最简单的实现。
对比这两种模式可以发现:
- 饿汉模式在类被加载时就将自己实例化,无需考虑多线程问题,在调用速度和反应时间方面要优于懒汉模式。
- 饿汉模式在系统加载时需要创建单例对象,因此加载时间会更长
- 懒汉模式第一次使用时创建,无需一直占用系统资源,实现了延迟加载
- 懒汉模式有多线程问题
2.3懒汉模式创建实例时加锁
为解决懒汉模式的线程安全问题,可以在getInstance()方法前增加synchronized关键字
示例代码
public class SingletonDemo3 {
private SingletonDemo3() {}
private static SingletonDemo3 instance = null;
public static synchronized SingletonDemo3 getInstance() {
if (instance == null) {
instance = new SingletonDemo3();
}
return instance;
}
}
这个类是线程安全的,但不推荐这种写法。因为每次调用getInstance()都要加锁,会使系统性能大大降低。
2.4 懒汉模式双重检查锁(Double-Check Locking)
为解决2.3中的性能问题,可以优化锁的粒度
示例代码
public class SingletonDemo4 {
private SingletonDemo4() {}
private static SingletonDemo4 instance = null;
public static SingletonDemo4 getInstance() {
if (instance == null) {
synchronized (SingletonDemo4.class) {
if (instance == null) {
instance = new SingletonDemo4();
}
}
}
return instance;
}
}
问题貌似得到了解决,但事实并非如此!!!这种实现方式存在线程安全问题。
我们对线程安全问题进行分析:
当执行instance = new SingletonExample4();这行代码时,CPU会执行如下指令:
1.memory = allocate() 分配对象的内存空间
2.ctorInstance() 初始化对象
3.instance = memory 设置instance指向刚分配的内存
单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。
假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new SingletonExample4(); B线程刚执行到第一个 if (instance == null){处,如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。
2.5 volatile+Double-Check Locking
提到解决指令重排序造成的线程安全问题,我们首先应该想到的就是volatile关键字
示例代码
public class SingletonDemo5 {
private SingletonDemo5() {}
private static SingletonDemo5 instance = null;
public static SingletonDemo5 getInstance() {
if (instance == null) {
synchronized (SingletonDemo5.class) {
if (instance == null) {
instance = new SingletonDemo5();
}
}
}
return instance;
}
}
被volatile修饰的成员变量是线程可见的,可解决线程安全问题。
这种实现方式只能在JDK1.5及以上版本中才能正确执行。volatile会导致系统的运行效率降低,因此这种实现方式并不完美。
2.6 使用静态内部类实现
为解决上述五种实现方式存在的线程安全或性能问题,使用静态内部类来实现单例模式。
示例代码
public class SingletonDemo6 {
private SingletonDemo6() {}
private static class HolderClass {
private final static SingletonDemo6 instance = new SingletonDemo6();
}
public static SingletonDemo6 getInstance() {
return HolderClass.instance;
}
}
这是一种饿汉模式,这种方式由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有加锁,因此不会有性能问题。
2.7 枚举方式进行实例化
示例代码
public class SingletonDemo7 {
private SingletonDemo7(){}
public static SingletonDemo7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonDemo7 singleton;
//JVM保证这个方法绝对只调用一次
Singleton(){
singleton = new SingletonDemo7();
}
public SingletonDemo7 getInstance(){
return singleton;
}
}
}
这种方式是线程最安全的方法
3、单例模式优缺点
3.1 优点
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以可以严格控制客户访问它的方式和方法
- 节约系统资源
3.2 缺点
- 没有抽象层,所以很难进行扩展
- 一定程度上违背了单一职责原则
- 长时间不用可能会被当成垃圾回收,导致共享的单例对象状态的丢失
4、适用场景
开发工具类库中的很多工具类都应用了单例模式,比如线程池、缓存、ID生成器、日志对象、网站的全局计数器等,它们都只需要创建一个对象。