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语义保证,单线程的有序性就天然存在了。