【设计模式】线程安全的单例模式
在工作中面试官会经常问你单例模式,而当你答出来的时候,又会接着让你写出线程安全的单例模式,下面我们就来学习一下,线程安全的单例模式的写法
首先我们知道单例模式有两种,分别是:
饿汉模式
public class SingletonExample2 {
//私有化构造函数
private SingletonExample2(){
}
//单例对象
private static SingletonExample2 instance = new SingletonExample2();
//静态工厂方法
public static SingletonExample2 getInstance(){
return instance;
}
}
懒汉模式
public class SingletonExample1 {
//私有化构造函数
private SingletonExample1(){
}
//单例对象
private static SingletonExample1 instance = null;
//静态工厂方法
public static SingletonExample1 getInstance(){
if (instance == null){
instance = new SingletonExample1();
}
return instance;
}
从写法上我们可以看出,饿汉模式是线程安全的,但它的性能上会大大折扣。那么我们能否也让懒汉模式也变得线程安全呢?
答案是可以的
方法一.在方法上加上同步锁
直接在获取实例的方法上加上synchronized关键字
public class SingletonExample3 {
//私有化构造函数
private SingletonExample3(){
}
//单例对象
private static SingletonExample3 instance = null;
//静态工厂方法
public synchronized static SingletonExample3 getInstance(){
if (instance == null){
instance = new SingletonExample3();
}
return instance;
}
}
虽说该方法是线程安全的,但其性能也和饿汉模式差不多,在性能上会大大折扣,别急我们接着看
方法二.使用双重校验加同步锁机制
public class SingletonExample4 {
//私有化构造函数
private SingletonExample4(){
}
//指令重排问题:
//1.分配内存空间
//2.初始化对象
//3.instance = memory设置instance指向刚分配的内存
//单例对象
private static SingletonExample4 instance = null;
//静态工厂方法
public static SingletonExample4 getInstance(){
if (instance == null){ //双重检测机制
synchronized (SingletonExample4.class) { //同步锁
if (instance == null){
instance = new SingletonExample4();
}
}
}
return instance;
}
}
通过两次判断,确保创建的对象只能有一个,但这种方法还是存在线程安全的问题的。
在单线程的情况下,以上的代码没有丝毫问题,但在多线程的情况下,就会存在指令重排问题
那么什么是指令重排呢?
要知道指令重排我们首先需要知道正常的创建对象的顺序
- 分配内存空间
- 初始化对象
- 将对象的引用指向新分配的空间
在JMM中会存在一种优化方案,在程序执行时,系统为了系统的性能优化,可能会通过将一些程序的内部的顺序打乱。这就是指令重排的问题,例如创建对象时,可能会将步骤2和3的顺序打乱,如下
- 分配内存空间
- 将对象的引用指向新分配的空间
- 初始化对象
这样就会存在问题了,如代码:
当A、B线程达到以上位置时,发生指令重排,在A线程执行到指令2(将对象的引用指向新分配的空间)时,刚好CPU被B占用,这样B的对象指向了一个内存空间,但其对象并没有被实例化
那么怎么样才能解决以上问题呢?
我们可以给代码加上一个volatile关键字来防止指令重排
//单例对象
private static volatile SingletonExample4 instance = null;
方法三.使用枚举模式来创建对象
有了以上方法为何还会需要第三种方法呢?那是因为java中还有一种暴力的创建方法,反射,虽然不能通过关键字new来创建对象,但通过反射创建的对象,就不会是单例的了,那么有什么办法可以解决吗?答案是有的,就是使用枚举。
public class SingletonExample7 {
private SingletonExample7(){}
public static SingletonExample7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample7 singleton;
//JVM保证这个方法绝对只调用一次
Singleton(){
singleton = new SingletonExample7();
}
public SingletonExample7 getInstance() {
return singleton;
}
}
}
好了,常见的几种线程安全的单例模式就介绍到这里了。如果有什么不足的地方,希望各位大佬能够指正。