什么是单例设计模式?
单例模式,是一种常见的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式的方法创建的类在当前进程中只有一个实例。
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
单例模式有以下特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有的其他对象提供这一实例
具体实现
需要:
- 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象
- 在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型
- 定义一个静态方法返回这个唯一对象
实现一:立即加载/“饿汉模式”
/**
* 饿汉式:在类初始化的时候,已经创建自己的实例
*/
public class Singleton {
//1,私有化构造方法
private Singleton(){}
//2,创建自己的单例对象
private final static Singleton singleton = new Singleton();
//3,提供获取当前类对象的方法
public static Singleton getInstance(){
return singleton;
}
}
优点: 线程安全,实现简单
缺点: 类一加载就初始化了对象,就算没有使用也一直占用着内存,会让内存开销变大
实现二:延迟加载/“饿汉模式”
/**
*
* 懒汉式:方法被调用的时候才实例化
*/
public class Singleton {
//1,私有化构造方法
private Singleton(){}
//2,静态私有的属性
private static Singleton singleton = null;
//3,提供获取当前类对象的方法
public static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
优点: 实现简单,当getInstance方法第一次被调用时菜初始化instance变量,并分配内存,因此在某些特定条件下节约了内存。
缺点: 线程不安安全
实现三:线程安全的“懒汉式”
public class Singleton {
// 构造方法私有化
private Singleton() {}
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 静态方法返回该实例,加synchronized关键字实现同步
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点: 在多线程情况下,保证了“懒汉模式”的线程安全
缺点: synchronized方法通常效率低,而且无论instance对象创建与否,线程都进行排队等候,效率低下。
实现四: DCL双检锁机制
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
// 第一次检查instance是否被实例化出来,如果没有进入if块
if(instance == null) {
synchronized (Singleton.class) {
// 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优点: 内存占用率高,效率高,线程安全
缺点: 存在指令重排问题
实现四中指令重排问题:
Singleton s = new Singleton();
执行上述语句时实际做了三件事:
1. 开辟内存空间
2. 实例化对象
3. 将内存地址返回
但在编译执行的时候会有指令重排问题,,编译器优化时为了减少内存的开销提高性能,执行顺序会变成:
1=>3=>2,即一开辟内存就将内存地址返回了,然后才实例化对象。
所以在实现四的单例模式中,可能出现以下情况:
如下图线程A拿到线程锁执行new语句时,开辟了内存空间并返回内存地址,
但是并没有实例化对象,此时线程B执行了外层判断对象s不为空,
就将其返回,但此时对象并没有实例化,就会出现问题。
image.png
实现五:加关键字volatile
public class Singleton {
// 保证了并发编程中的有序性
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查instance是否被实例化出来,如果没有进入if块
if(instance == null) {
synchronized (Singleton.class) {
// 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注意:
需要加volatile关键字的原因是,在并发情况下,如果没有这个关键字,
在第5行会出现问题。因为第五行代码“instance = new Singleton()”
并不是原子性操作,在JVM中被分为如下三个阶段执行:
1. 为instance分配内存
2. 初始化instance
3. 将instance变量指向分配的内存空间
由于JVM可能存在重排序,可能会执行第3步然后再执行第2步。也就是说可能
会出现instance变量还没有初始化完成,其他线程就已经判断了该变量不为null,
结果返回了 一个没有初始化的半成品。而加上volatile关键字,可以保证instance
变量的操作不会被JVM重排。
网友评论