我们先来看下双重校验模式的标准代码:

public class Singleton1 {
    private static volatile Singleton1 singleton;

    private Singleton1(){}

    public static Singleton1  getStance(){
        if(singleton == null){

            synchronized (Singleton1.class){
                if (singleton == null){
                    singleton = new Singleton1();
                }
            }
        }
        return singleton;
    }
}

其次,我们应该知道,synchronized 能保证临界区的原子性、有序性和可见性。volatile 也能保证所修饰对象的可见性,并且还能禁止重排序。
那么问题就来了:既然 volatile 的功能 synchronized基本都具备,那为啥还需要 volatile 修饰单例对象呢?
我找了很多资料和博客,基本都是解释 new 操作不是原子操作,在 JVM 层面会导致重排序,但是这并不能解释为什么 volatile 和 synchronized 关于有序性功能的重叠。

      public static Singleton1  getStance(){
      
        if(singleton == null){ // #1
            synchronized (Singleton1.class){
                if (singleton == null){
                    singleton = new Singleton1(); //#2
                }
            }
        }
        return singleton;
    } 
      // 当两个线程A和B同时进入方法时,加入A抢夺到锁,则A继续执行,当A执行到new操作时,由于new操作不是原子操作,且synchronized也不能禁止重排序,
      // 我们首先将new操作原子化:a-开辟内存空间;b-初始化对象;c-将引用赋值给变量
      // 正常的执行顺序应该是a-b-c,不禁止重排序的情况下可能是:a-c-b
      // 当线程A执行a-c,即将执行b的时候,由于cpu时间片结束,则有可能会让步给线程B,
      // 线程B进行第一次判断,singleton由于已经有了内存指向,并不为空,此时,对象还没有执行初始化,但已经判断为true,并且返回了。
      // 此时,就产生了严重的错误,因此需要 volatile 来禁止重排序。
      

关于这个问题,我思考良久,最后我找到 synchronized 关于有序性的解释:只能保证有序性却不能禁止重排序。
很多博客解释了很多,我起初非常不能理解,因为都没提到synchronized只能保证有序性却不能禁止重排序。我觉得这句话才是解释这个问题的关键所在。

posted on 2020-06-08 18:58  曹战  阅读(792)  评论(0编辑  收藏  举报