<span>单例模式</span>
💛原文地址为https://www.cnblogs.com/haixiang/p/12063951.html,转载请注明出处!
简介
单例模式主要是为了避免因为创建了多个实例造成资源的浪费,且多个实例由于多次调用容易导致结果出现错误,而使用单例模式能够保证整个应用中有且只有一个实例。
要求
只需要三部分即可保证只有一个实例
- 不允许被外部类 new 对象
- 在本类中创建对象
- 对外提供接口调用创建好的对象
实现思路
针对上述三个条件我们用三种方式来保证这种要求
- 构造方法私有化
- 在本类中 new 一个本类对象
- 创建一个 public 方法返回上步创建好的对象以供外部调用
懒汉式单例模式
这种方式在类创建的时候就实例化对象,不存在多线程问题,缺点是提前创建对象,若未被使用会造成资源浪费。
public class SingletonIns {
private static SingletonIns singletonIns = new SingletonIns();
private SingletonIns() {}
public static SingletonIns getInstance() {
return singletonIns;
}
}
饿汉式单例模式
饿汉式单例优缺点与懒汉式正好相反,如果不作处理会有并发的问题,但是按需实例化,能避免资源被浪费的情况出现。
以下是最佳实践的例子
public class SingletonIns {
private static volatile SingletonIns singletonIns = null;
private SingletonIns() {}
public static SingletonIns getInstance() {
if (null == singletonIns) {
synchronized (SingletonIns.class) {
if (null == singletonIns) {
singletonIns = new SingletonIns();
}
}
}
return singletonIns;
}
}
以下这种方式效率较低,在jdk以前的版本中synchronized
性能极差,后续有了自旋锁、偏量锁等优化才慢慢改善,现在仍然不建议这样使用单例模式
public class SingletonIns {
private static SingletonIns singletonIns = null;
private SingletonIns() {}
public synchronized static SingletonIns getInstance() {
if (null == singletonIns) {
singletonIns = new SingletonIns();
}
return singletonIns;
}
}
以下这种方式存在线程安全的问题,例如a线程进入if判断,b线程已经进入2处,会导致实例化两个不同的对象
public class SingletonIns {
private static SingletonIns singletonIns = null;
private SingletonIns() {}
public static SingletonIns getInstance() {
if (null == singletonIns) { //1 有线程安全问题
synchronized(SingletonIns.class) {
singletonIns = new SingletonIns();//2
}
}
return singletonIns;
}
}
以下这种方式使用双重校验来避免上一种线程安全问题的出现,但是仍然存在线程安全问题。这是因为在线程执行到第1处的时候,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化
public class SingletonIns {
private static SingletonIns instance = null;
private SingletonIns() {}
public SingletonIns getInstance() {
if (null == instance) { //1
synchronized(SingletonIns.class) {
if(null == instance)
instance = new SingletonIns();//2
}
}
return instance;
}
}
为什么会出现这样的问题呢?主要的原因是重排序。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
第2处的代码创建了一个对象,这一行代码可以分解成3个操作:
Copymemory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
根源在于代码中的2和3之间,可能会被重排序。例如:
Copymemory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象java
这在单线程环境下是没有问题的,但在多线程环境下会出现问题:B线程会看到一个还没有被初始化的对象。
A2和A3的重排序不影响线程A的最终结果,但会导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。
重排序能够保证单线程的情况下,执行结果与按顺序执行结果一致,但是无法保证多线程下结果正确。所以我们需要使用volatile
来修饰instance
,来禁用重排序,保证线程安全