代码之家  ›  专栏  ›  技术社区  ›  nyarian

易失性读取冲突

  •  1
  • nyarian  · 技术社区  · 6 年前

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

    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;
    }
    

    类只会被实例化一次吗?或者换句话说,volatile读取会以两个线程可以看到的方式冲突 null 将执行引用值和双实例化?

    我知道 以前发生过 volatile write和volatile read之间的关系,volatile禁止缓存(因此所有读写都将在主内存中执行,而不是在处理器的缓存中执行),但在并发volatile read s的情况下,这一点并不清楚。

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

    4 回复  |  直到 6 年前
        1
  •  1
  •   user10367961    6 年前

    考虑到这一准则。

    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)。
        2
  •  2
  •   Naomi    6 年前

    如果没有同步,您的代码肯定会被破坏,因为2个线程可能会看到实例的值为空,并且这两个线程都将执行初始化(考虑每行都有一个上下文切换,然后看看会发生什么)。

    除此之外,即使是同步的双重检查锁定(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;
    }
    

    但是一个更安全的解决方案是使用类加载器作为同步机制,并且还允许您在每次访问单例时停止使用慢速易失性访问:

    public class Instance {
    
        private static class Lazy {
            private static Instance INSTANCE = new Instance();    
        }
    
        public static Instance getInstance() {
            return Lazy.INSTANCE;
        }
    }
    

    INSTANCE 只有当第一个线程进入时才会初始化 getInstance()

        3
  •  2
  •   Krzysztof Cichocki    6 年前

    是的,确实如此:易失性读取可能会发生冲突,以至于两个线程将看到引用的空值,并且将执行双实例化。

    您还需要双括号初始化和volatile。 那是因为什么时候 instance 变为非空,在读取任何内容之前不同步其他线程-首先 if 只是让他们走得更远 unsynchronized 值(即使初始化线程尚未脱离同步块),这可能导致后续线程读取未初始化的变量,因为缺少同步。要使同步正常工作,需要由每个访问它所控制的数据的线程执行,DCL在初始化后省略了同步,这是错误的。这就是为什么您需要额外的volatile来让DCL工作,那么volatile将确保您读取初始化的值。

    没有像处理器缓存分离这样的东西,读和写都是可见的,但是有指令重新排列,所以为了优化,如果处理器不需要它们的结果,可以稍后调用一些指令。同步和易失性的关键是不要重新排列进入它们的线程的指令顺序。这样,如果某个东西是同步的,并且在代码中声明为完成的,那么它确实完成了,其他线程可以安全地访问它。这就是保证之前发生的全部事情。

    将其汇总在一起:如果没有适当的同步处理器,则可以初始化对 实例 非空,但 实例 未能在内部完全初始化,因此后续的线程读取它时可能会读取未初始化的对象,因此行为不正确。

        4
  •  0
  •   marstran    6 年前

    如果你真的需要Java中的一个单元格,可以使用枚举。它为您修复了以下问题:

    enum MySingleton {
        INSTANCE;
    
        public static MySingleton getInstance() {
            return INSTANCE;
        }
    }
    

    在第一次访问时,JVM将以线程安全的方式初始化实例。 MySingleton . 获取它的多个实例的唯一方法是使用多个类加载器。