猿问

易失性读取冲突

假设我们正在使用双重检查锁定实例化一个单例:


public static Instance getInstance() {

    if (this.instance == null) {

        synchronized(Instance.class) {

            if (this.instance == null) {

                this.instance = new Instance();

            }

        }

    }

    return this.instance;

}

问题在于程序的语义,如果instance变量是可变的并且双重检查锁定将被删除。


private volatile static Instance instance;


public static Instance getInstance() {

    if (this.instance == null) {

        this.instance = new Instance();

    }

    return this.instance;

}

类只会实例化一次吗?或者,换句话说,易失性读取是否会以这样一种方式发生冲突,即两个线程将看到null引用的值并执行双重实例化?


我知道易失性写入和易失性读取之间的发生在之前的关系,并且易失性禁止缓存(因此所有读取和写入都将在主内存中执行,而不是在处理器的缓存中),但在这种情况下尚不清楚并发易失性读取。


PS:问题不在于单例模式的应用(这只是一个问题很明显的例子),只是关于双重检查锁定是否可以替换为易失性读取 - 易失性写入而不改变程序语义,仅此而已比起那个来说。


弑天下
浏览 156回答 3
3回答

天涯尽头无女友

如果没有同步,你的代码肯定会被破坏,因为 2 个线程可能会看到 instance 的值为 null,并且都将执行初始化(考虑在每一行进行上下文切换,看看会发生什么。除此之外,即使同步双重检查锁定(DCL)在过去在 Java 中也被认为是坏的,因为在非同步运行时,第二个线程可能会以不同的顺序进行初始化操作。你可以通过添加一个局部变量来修复你的代码,并在你想要读取它时将 volatile 加载到其中:public static Instance getInstance() {    Instance tmp = instance;    if (tmp == null) {        synchronized(Instance.class) {            Instance tmp = instance;            if (tmp == null) {                instance = new Instance();            }        }    }    return instance;}但更安全的解决方案是使用 ClassLoader 作为您的同步机制,并且还允许您在每次访问单例时停止使用慢速 volatile 访问:public class Instance {    private static class Lazy {        private static Instance INSTANCE = new Instance();        }    public static Instance getInstance() {        return Lazy.INSTANCE;    }}INSTANCE 只有在第一个线程进入时才会被初始化 getInstance()

白猪掌柜的

是的,确实:易失性读取可能会以这样的方式发生冲突,即两个线程将看到引用的空值并执行双重实例化。您还需要双括号初始化和 volatile。那是因为当instance变为非 null 时,您在读取它之前不会在任何东西上同步其他线程 - 首先if只是让它们进一步返回unsynchronized值(即使初始化线程还没有转义同步块),这可能导致由于缺乏同步,后续线程读取未初始化变量。同步工作需要每个线程访问它所管理的数据来执行,DCL 在初始化后忽略了同步,这是错误的。这就是为什么您需要额外的 volatile 才能使 DCL 工作,然后 volatile 将确保您读取初始化值。没有处理器缓存分离这样的东西,读取和写入是立即可见的,但是有指令重新排列,因此有利于优化处理器如果不需要立即调用它们的结果,则可以稍后调用一些指令。同步和 volatile 的全部意义在于不要重新排列访问它们的线程的指令顺序。这样,如果某事已同步并在代码中声明为已完成,则它确实已完成并且其他线程可以安全地访问它。这就是在保证之前发生的全部意义。总结一下:没有适当的同步处理器可以将引用初始化instance为非空,但instance不能在内部完全初始化,因此后续线程读取它可能会读取未初始化的对象并因此行为错误。

慕工程0101907

考虑到这段代码。private volatile static Instance instance;public static Instance getInstance() {    if (this.instance == null) {        this.instance = new Instance();    }    return this.instance;}从你的问题:Will the class get instantiated only once? Can volatile reads clash in such way that two threads will see null value of the reference and double instantiation will be performed?在 JMM 的保证之外,易失性读取不能以这种方式发生冲突。但是,如果多个线程在 if 之后但在开始实例化 volatile 变量之前交换,您仍然可以得到两个实例。if (this.instance == null) {    // if threads swap out here you get multiple instances    this.instance = new Instance();}为了确保上述场景不会发生,你必须使用双重检查锁定if (this.instance == null) {    // threads can swap out here (after first if but before synchronized)    synchronized(Instance.class) {        if (this.instance == null) {            // but only one thread will get here            this.instance = new Instance();        }    }}请注意,这里必须考虑两个方面。原子性:我们需要确保第二个 if 和实例化以原子方式发生(这就是我们需要synchronized块的原因)。可见性:我们需要确保对实例变量的引用不会在不一致的状态下转义(这就是为什么我们需要volatile实例变量的声明以利用 JMM 在保证之前发生)。
随时随地看视频慕课网APP

相关分类

Java
我要回答