文章目录
- DCL单例模式 起源过程
- 双重检查加锁单例模式为什么两次if判断?
- java 单例模式中双重检查锁定 volatile 的作用?
DCL单例模式 起源过程
DCL单例模式
参考URL:
我们第一次写的单例模式是下面这样的:
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // line A
instance = new Singleton(); // line B
}
return instance;
}
}
假设这样的场景:两个线程并发调用Singleton.getInstance(),假设线程一先判断instance是否为null,即代码中line A进入到line B的位置。刚刚判断完毕后,JVM将CPU资源切换给线程二,由于线程一还没执行line B,所以instance仍然为空,因此线程二执行了new Singleton()操作。片刻之后,线程一被重新唤醒,它执行的仍然是new Singleton()操作,这样问题就来了,new出了两个instance,这还能叫单例吗?
我们再做单例模式的第二次尝试,
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
return instance;
}
}
比起第一段代码仅仅在方法中多了一个synchronized修饰符,现在可以保证不会出现刚才线程切换引起的问题了。但是这个性能比较低,每次获取instance,都要进行synchronized同步准备工作。
解决思路:
除了第一次调用时是执行了Singleton的构造函数之外,以后的每一次调用都是直接返回instance对象。返回对象这个操作耗时是很小的。
我们只希望在第一次创建instance实例的时候进行同步,因此有了下面的写法——双重锁定检查(DCL,Double Check Lock)
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // 线程二检测到instance不为空
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
}
}
}
return instance; // 后面线程二执行时将引发:对象尚未初始化错误
}
}
看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个if中就返回了,因此不会走到同步块中,已经完美了吗?
如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:
1)给instance实例分配内存;
2)初始化instance的构造器;
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量。
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
将变量instance使用volatile修饰即可实现单例模式的线程安全。
双重检查加锁单例模式为什么两次if判断?
内层判断:如果内层不加if判断,就会实例化多次,这是显而易见的,这就违背了单例模式的单例二字。(因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。)
外层判断:试图想想一种情况,当线程1走完了内层判断,对象实例化了,线程3也调用了getInstace函数,如果没有加外层的判断线程3还是要继续等待线程2的完成,而加上外层判断,就不需要等待了,直接返回了实例化的对象。
总结:外层的判断是为了提高效率,里层的判断就是第一次实例化需要。
java 单例模式中双重检查锁定 volatile 的作用?
[推荐阅读]java 单例模式中双重检查锁定 volatile 的作用?
参考URL: https://www.zhihu.com/question/56606703?sort=created
volatile 是保证了可见性还是有序性?
主要是禁止重排序,初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤,
- 申请内存空间,
- 初始化默认值(区别于构造器方法的初始化),
- 执行构造器方法
- 连接引用和实例。
这4个步骤后两个有可能会重排序,1234 1243都有可能,造成未初始化完全的对象发布。
为什么要禁止重排序?
确保先执行构造器方法,再将引用和实例连接到一起。如果没有禁止重排序,会导致另一个线程可能获取到尚未构造完成的对象。
为什么没有起到可见性的作用?
JSR-133
An unlock on a monitor happens before every subsequent lock on that same monitor
第二次非null判断是在加锁以后,则根据这一条,另一个线程一定能看到这个引用被赋值。所以即使没有volatile,依旧能保证可见性。
总结: DCL单例中 1、synchronized已经保证了可见性,不需要volatile来保证。2、volatile的作用是禁止重排序。 因此,主要是为了保证原子操作。