欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java | 单例模式中双重校验锁的volatile变量有什么作用?

程序员文章站 2024-02-29 18:31:34
...

单例模式的双重校验锁方式如下:

/**
 * 双重校验锁(在饿汉模式基础上进一步优化)
 * 1、构造方法私有化
 * 2、在定义静态对象时加volatile锁来确保初始化时对象的唯一性
 * 3、定义获取对象实例方法,并在方法体中通过synchronized(Object)给单例类加锁来保障操作的唯一性
 * */
class DoubleCheckedLockingSingleton{
    private volatile static DoubleCheckedLockingSingleton instance;
    private DoubleCheckedLockingSingleton(){}
    public static DoubleCheckedLockingSingleton getInstance(){
        if(instance == null){
            synchronized (DoubleCheckedLockingSingleton.class){
                if(instance == null)
                    instance = new DoubleCheckedLockingSingleton();
            }
        }
        return instance;
    }
}

 

如下,instance为什么需要volatile修饰?有什么作用?解决了什么问题?

 private volatile static DoubleCheckedLockingSingleton instance;

单例模式中使用volatile修饰时可使用双重校验锁方式,至于定义静态对象时为什么需要加volatile来修饰,如代码注释所述,volatile确保初始化时对象的唯一性。那么如果不使用volatile修饰,会出现什么问题了?

在考虑问题时,先学习下几个相关知识点,

第一个:volatile是什么?

Java中提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程,当变量声明为volatile后,则编译时与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。

第二个:什么是竞态条件?

在Java多线程中,如果没有正确的使用同步会出现意想不到的糟糕情况。而竞态条件是指在多线程环境下由于不恰当的执行时序而出现不正确的结果。

第三个:什么是延迟初始化?

延迟初始化的目的是将对象的初始化操作推迟到实际被使用时才执行,同事要确保只能被初始化一次。

第四个:什么是重排序?

在没有充分同步的条件下,如果调度器采用不恰当的方式来交替执行不同的线程的操作,将会导致不正确的结果。因此各种操作延迟或者看似乱序执行的不同原因,都可以归为重排序。

第五个:什么是Happens-Before?

JMM为程序中所有的操作定义了一个偏序关系,即Happens-Before。若想确保线程B正确看到线程A操作的结果,那么A与B之间必须满足偏序关系。即确保线程之间表现出串行一致性。


那么在回归到这个问题上,如果不为instance添加volatile修饰符,则其工作过程是首先检查是否在没有同步的情况下需要初始化,如果instance引用不为空(null),那么直接使用它即可。否则就进行同步并再次检查instance是否被初始化,从而保证只有一个线程对共享的instance执行初始化。但真正的问题是,假设线程A先访问getInstance()时已获取一个构造好的instance引用,但它并没有使用同步。而在没有同步的情况下读取一个共享对象时,可能看到一个失效值。即在没有正确同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。即刻线程B访问getInstance()方法,而此时线程B访问时它可能看到instance仍然为空。即线程A与线程B之间不存在Happens-Before关系时,且发布对象时没有正确使用同步时,线程B并不一定能真正看到instance的正确状态。

针对单例模式,更倾向于使用延迟初始化占位类模式,通常被成为静态内部类完成单例模式,如下:

/**
 * 静态内部类(类的静态内部类在JVM中是唯一)
 * 1、构造方法私有化
 * 2、在类中定义一个静态内部类,并在内部类中完成对象实例的定义和初始化
 * 3、定义获取对象方法,并通过静态内部类调用其单例对象
 */
class OuterSingleton{
    private static class InnerSingleton{
        private static final OuterSingleton INSTANCE = new OuterSingleton();
    }
    private OuterSingleton(){}
    public static final OuterSingleton getInstance(){
        return InnerSingleton.INSTANCE;
    }
}

为什么使用该方式?该方式中使用一个专门的类即InnerSingleton来初始化OuterSingleton。而JVM中将推迟InnerSingleton的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化INSTANCE,因此不需要额外的同步。当任何一个线程第一次调用getInstance() 时,都会使InnerSingleton被加载和被初始化,此时静态初始化器将执行OuterSingleton的初始化操作。静态初始化器是由JVM在类初始化阶段执行,即在被类加载后并且被线程使用之前。

 

 

相关标签: Java