双重校验锁实现的单例,已经使用了synchronized,为什么还需要volatile?
Q1:有了synchronized
为何要用volatile?
synchronized
能保证临界区的原子性、有序性和可见性。volatile
也能保证所修饰对象的可见性,并且还能禁止重排序。
那么问题就来了:既然 volatile
的功能 synchronized
基本都具备,那为啥还需要 volatile
修饰单例对象呢?
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
答:new 操作不是原子操作,在 JVM 层面会导致重排序。synchronized
关于有序性的准确解释:synchronized
只能保证有序性却不能禁止重排序。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { // #1
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // #2
}
}
}
return singleton;
}
}
// 1.当两个线程A和B同时进入方法时,加入A抢夺到锁,则A继续执行,当A执行到new操作时,由于new操作不是原子操作,且synchronized也不能禁止重排序,
// 2.我们首先将new操作原子化:a-开辟内存空间;b-初始化对象;c-将引用赋值给变量
// 3.正常的执行顺序应该是a-b-c,不禁止重排序的情况下可能是:a-c-b
// 4.当线程A执行a-c,即将执行b的时候,由于cpu时间片结束,则有可能会让步给线程B
// 5.线程B进行第一次判断,singleton由于已经有了内存指向,并不为空,此时,对象还没有执行初始化,但已经判断为true,并且返回了。
// 6.此时,就产生了严重的错误,因此需要 volatile 来禁止重排序。
我们可以理解为没有volatile
关键字时,当synchronized
临界区中线程1出现CPU时间片耗尽被终止,而在该线程被终止之前已经执行了singleton = new Singleton();
语句(即在#2处执行完毕之前被终止)的情况时,可能会出现对象已经分配了内存,但未初始化,此时线程2进入临界区在进行第一个null检查(标记为#1处)时,发现singleton
不为null,就会直接返回singleton
,导致出现错误。
Q2:既然synchronized
无法禁止指令重排,那synchronized
可以保证有序性怎么理解?
答:这个有序性是相对语义来看的,线程与线程间,每一个 synchronized
块可以看成是一个原子操作,它保证每个时刻只有一个线程执行同步代码,相当于单线程,而单线程的指令重排是没有问题的。这就满足了as-if-serial
语义的一个关键前提,那就是单线程,因为有as-if-serial
语义保证,单线程的有序性就天然存在了。
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果