易失性读取冲突

时间:2018-10-22 13:03:06

标签: java concurrency volatile double-checked-locking

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

public static Instance getInstance() {
    if (this.instance == null) {
        synchronized(Instance.class) {
            if (this.instance == null) {
                this.instance = new Instance();
            }
        }
    }
    return this.instance;
}

问题在于程序的语义,如果instance变量是 volatile 并且将取消双重检查锁定。

private volatile static Instance instance;

public static Instance getInstance() {
    if (this.instance == null) {
        this.instance = new Instance();
    }
    return this.instance;
}

该类仅实例化一次吗?或者,换一种说法,volatile是否可以以两个线程看到引用的null值并执行双重实例化的方式发生冲突?

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

PS:问题不在于应用Singleton模式(这只是一个明显的问题示例),而是可以用易失性读取替换双重检查锁定-无需更改程序语义即可进行易失性写入,仅此而已。

4 个答案:

答案 0 :(得分:2)

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

时被初始化

答案 1 :(得分:2)

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

您还需要双括号初始化和volatile。 这是因为当instance变为非null时,您不会在读取任何内容之前同步其他线程-首先if只是使它们进一步返回unsynchronized值(即使初始化)线程尚未逸出同步块),这可能导致后续线程由于缺少同步性而未初始化变量读取。要使同步正常工作,必须由每个线程访问由其控制的数据来执行,DCL在初始化后忽略了同步,这是错误的。这就是为什么您需要额外的volatile才能使DCL正常工作,然后volatile将确保您读取初始化值。

没有诸如处理器高速缓存分离之类的东西,读取和写入立即可见,但是有指令重排,因此有利于优化的处理器可以在稍后不需要其结果的情况下及时调用某些指令。 。同步和易失性的全部重点是不要重新安排线程访问指令的顺序。这样,如果某事已同步并在代码中声明为已完成,则实际上已完成,其他线程可以安全地访问它。这就是保证之前发生的全部事情。

总结起来:没有适当的同步处理器,可以将对instance的引用初始化为非null,但是instance可能没有在内部完全初始化,因此后续的线程读取可以读取未初始化的对象,并且因此会做出错误的举动。

答案 2 :(得分:1)

考虑此代码。

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)。

答案 3 :(得分:0)

如果您确实需要Java中的单例,请使用一个枚举。它可以为您解决这些问题:

enum MySingleton {
    INSTANCE;

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

JVM将在首次访问MySingleton时以线程安全的方式初始化实例。获得多个实例的唯一方法是,如果您有多个类加载器。