单例懒汉实现解决
双重校验锁
理解了线程不安全的原因,以及线程同步的性能消耗原理,因此就需要对其作出改进,在保障线程安全的前提下,又能进一步的减少消耗。
其实不难发现,我们只要保证instance = new SingletonOne();
是线程互斥访问的就可以保证线程安全了。
那把同步方法加以改造,只用synchronized
块包裹这一句。就可以进行:
package com.jachie.singleton;
public class SingletonThree {
private SingletonThree() {}
private static SingletonThree instance = null;
public static SingletonThree getInstance() throws InterruptedException {
// 第一次判断是否为空
if(instance == null) { // 1.
synchronized (SingletonThree.class){
instance = new SingletonThree(); // 2.
}
}
return instance;
}
}
还没搞定
可是这样并没有解决问题,A、B线程同时进入代码1.
的位置的时候,产生互斥,而此时instance
依然是空的,而A和B会分别先后去执行synchronized
代码块,创建实例,从而依然会创建两个实例,因此我们需要在代码块里,也就是2.
之前再进行一次判断校验,判断instance
是否为空,这样第二次执行synchronized
代码块的就可以有一个判断不会产生新的实例。
package com.jachie.singleton;
public class SingletonThree {
private SingletonThree() {}
private static SingletonThree instance = null;
public static SingletonThree getInstance() throws InterruptedException {
// 第一次判断是否为空
if(instance == null) {
synchronized (SingletonThree.class){
// 第二次判断是否为空,防止线程在第一次和第二次之间进行了时间片的转换而导致产生两个实例
if(instance == null) {
instance = new SingletonThree();
}
}
}
return instance;
}
}
如此,当线程A执行结束后,线程B再进入synchronized
块后,会先检查一下instance
实例是否被创建,这时实例已经被线程A创建过了。所以线程B不会再创建实例,而是直接返回。
JVM重排可能导致异常
原本以为就此结束,可是查阅资料以及访问其他博客发现竟然还有个问题,那就是无序写(out-of-order writes)
机制,当前java平台的内存模型,在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
而我们创建对象——instance = new SingletonThree();
的操作也需要多条指令才可以完成,可以简单划分为:
- 为
singleton对象
分配内存空间- 初始化
singleton对象
- 将这块内存地址,指向
singleton对象
。
而由于JVM具有指令重排的特性,这只是我们理想创建对象的操作,如果指令进行了重排,执行顺序有可能变成 1-3-2
。 指令重排在单线程下不会出现问题,但是在多线程下就会导致返回一个未被初始化的对象:
辟如
:
线程1
执行了1和3两个操作,此时线程2
进来进行操作,当它调用getInstance()
时的时候发现singleton对象
不为空,则返回,而此时的singleton对象
并没有初始化,因此容易造成程序的异常。
既然发现了问题,找到了原因,那么就对症下药,因此,根本方法就是组织JVM对指令的重排操作,他该怎么走,就怎么走,按照顺序来搞。
volatile
- 被
volatile
关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile
变量不会被缓存在寄存器或者对其他处理器不可见的地方,是保证了JVM 每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取volatile
类型的变量时总会返回最新写入的值。volatile
在Java并发编程中常用于保持内存可见性
和防止指令重排序
。
那么,这样我们就比较好理解,需要要加入Volatile
变量了。由于Volatile禁止JVM对指令进行重排序。所以创建对象的过程仍然会按照指令1-2-3的有序执行。
最终优化
不多说,干他
package com.jachie.singleton;
public class SingletonThree {
private SingletonThree() {}
// 使用 volatile 修饰保证不会被重排序
private volatile static SingletonThree instance = null;
public static SingletonThree getInstance() throws InterruptedException {
// 第一次判断是否为空
if(instance == null) {
synchronized (SingletonThree.class){
// 第二次判断是否为空,防止线程在第一次和第二次之间进行了时间片的转换
if(instance == null) {
instance = new SingletonThree();
}
}
}
return instance;
}
}