emmm, 单例应该是最常见的设计模式了,我们比较简单的写法就是延迟初始化的写法,昨天在看《Java并发编程实战》的时候,发现原来延迟初始化是一种线程不安全的写法,回寝室的路上想了想,早上也看了些资料,今天简单做个总结。
#####1.延迟初始化
@NotThreadSafe
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码
如果单例初始值是null,则new一个单例并返回。这个写法属于单例模式当中的懒汉模式。如果我们一开始写成new Singleton(),则不再需要判空操作,这种写法属于饿汉模式。这种方法在单线程的情况是可以,但在多线程的情况下,是不安全。
原因如下: 假设我们Singleton类刚刚被初始化,instance对象为空,这时候两个线程同时访问到了getInstance方法,那他们都会通过if判断,然后会都会返回给instance,那我们两个getInstance可能得到不同的结果
#####2.简单修改
@ThreadSafe
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码
最简单的解决方法当然是直接对整个方法加synchronized关键字,但是这样效率会很低,我们要尽量避免对整个方法加锁,所以可以想到用操作系统里面的双重检测修改后如下
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance(){
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
复制代码
一开始我以为我也以为这样处理就行了,很多博客里面都说这种也已经是线程安全的写法,然后有人在评论指出了这种其实也不是绝对线程安全。
原因如下: 虽然看起来是可以的,但JVM编译器会进行指令重排,比如 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间 instance =memory; //3:设置instance指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象
如果多个线程同时访问的时候,A线程执行到了13,B线程进行if判断,虽然instance还没初始化,但他已经不为空,B线程就会返回一个未初始化的instance,防止指令重排有什么办法? 加volatile注解啊。所以只要定义变量的时候写为: private volatile static Singleton instance = null; 就可以了。