为了账号安全,请及时绑定邮箱和手机立即绑定

易失性读取冲突

易失性读取冲突

弑天下 2021-12-01 16:52:45
假设我们正在使用双重检查锁定实例化一个单例: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:问题不在于单例模式的应用(这只是一个问题很明显的例子),只是关于双重检查锁定是否可以替换为易失性读取 - 易失性写入而不改变程序语义,仅此而已比起那个来说。
查看完整描述

3 回答

?
天涯尽头无女友

TA贡献1831条经验 获得超9个赞

如果没有同步,你的代码肯定会被破坏,因为 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()


查看完整回答
反对 回复 2021-12-01
?
白猪掌柜的

TA贡献1893条经验 获得超10个赞

是的,确实:易失性读取可能会以这样的方式发生冲突,即两个线程将看到引用的空值并执行双重实例化。

您还需要双括号初始化和 volatile。那是因为当instance变为非 null 时,您在读取它之前不会在任何东西上同步其他线程 - 首先if只是让它们进一步返回unsynchronized值(即使初始化线程还没有转义同步块),这可能导致由于缺乏同步,后续线程读取未初始化变量。同步工作需要每个线程访问它所管理的数据来执行,DCL 在初始化后忽略了同步,这是错误的。这就是为什么您需要额外的 volatile 才能使 DCL 工作,然后 volatile 将确保您读取初始化值。

没有处理器缓存分离这样的东西,读取和写入是立即可见的,但是有指令重新排列,因此有利于优化处理器如果不需要立即调用它们的结果,则可以稍后调用一些指令。同步和 volatile 的全部意义在于不要重新排列访问它们的线程的指令顺序。这样,如果某事已同步并在代码中声明为已完成,则它确实已完成并且其他线程可以安全地访问它。这就是在保证之前发生的全部意义。

总结一下:没有适当的同步处理器可以将引用初始化instance为非空,但instance不能在内部完全初始化,因此后续线程读取它可能会读取未初始化的对象并因此行为错误。


查看完整回答
反对 回复 2021-12-01
?
慕工程0101907

TA贡献1887条经验 获得超5个赞

考虑到这段代码。


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 在保证之前发生)。


查看完整回答
反对 回复 2021-12-01
  • 3 回答
  • 0 关注
  • 155 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信