【问题标题】:double check lock without volatile is wrong?没有volatile的双重检查锁错了?
【发布时间】:2021-07-04 14:56:59
【问题描述】:

我使用 jdk1.8。我认为没有 volatile 的双重检查锁是正确的。 我多次使用 countdownlatch 测试,对象是单例的。 如何证明它一定需要“volatile”?

更新 1

抱歉,我的代码没有格式化,因为我无法接收一些 JavaScript 公共类 DCLTest {

private static /*volatile*/ Singleton instance = null;

static class Singleton {

    public String name;

    public Singleton(String name) {
        try {
            //We can delete this sentence, just to simulate various situations
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.name = name;
    }
}

public static Singleton getInstance() {
    if (null == instance) {
        synchronized (Singleton.class) {
            if (null == instance) {
                instance = new Singleton(Thread.currentThread().getName());
            }
        }
    }
    return instance;
}

public static void test() throws InterruptedException {
    int count = 1;
    while (true){
        int size = 5000;
        final String[] strs = new String[size];
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < size; i++) {
            final int index = i;
            new Thread(()->{
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Singleton instance = getInstance();
                strs[index] = instance.name;
            }).start();
        }
        Thread.sleep(100);
        countDownLatch.countDown();
        Thread.sleep(1000);
        for (int i = 0; i < size-1; i++) {
            if(!(strs[i].equals(strs[i+1]))){
                System.out.println("i = " + strs[i] + ",i+1 = "+strs[i+1]);
                System.out.println("need volatile");
                return;
            }
        }
        System.out.println(count++ + " times");
    }
}

public static void main(String[] args) throws InterruptedException {
    test();
}

}

【问题讨论】:

标签: java concurrency singleton volatile double-checked-locking


【解决方案1】:

您没有看到的关键问题是指令可以重新排序。因此它们在源代码中的顺序与它们在内存中的应用顺序不同。 CPU 和编译器是造成这种重新排序的原因。

我不会详细介绍双重检查锁定示例的整个示例,因为有很多示例可用,但会为您提供足够的信息来进行更多研究。

如果你有以下代码:

 if(singleton == null){
     synchronized{
         if(singleton == null){
            singleton = new Singleton("foobar")
         }
     }
 }

然后在引擎盖下会发生这样的事情。

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            tmp.value = "foobar"
            singleton = tmp
         }
     }
 }

到目前为止,一切都很好。但以下重新排序是合法的:

if(singleton == null){
     synchronized{
         if(singleton == null){
            tmp = alloc(Singleton.class)
            singleton = tmp
            tmp.value = "foobar"
         }
     }
 }

所以这意味着一个尚未完全构造的单例(值尚未设置)已写入单例全局变量。如果另一个线程读取这个变量,它可以看到一个部分创建的对象。

还有其他潜在的问题,例如原子性(例如,如果值字段很长,则它可能是碎片化的,例如读/写撕裂)。还有能见度;例如编译器可以优化代码,以便优化内存中的加载/存储。请记住,从内存而不是缓存中读取的想法从根本上是有缺陷的,也是我在 SO 上看到的最常见的误解;甚至很多前辈都搞错了。原子性、可见性和重新排序是 Java 内存模型的一部分,并且使单例变量 volatile 解决了所有这些问题。它消除了数据竞争(您可以查看更多详细信息)。

如果你想成为真正的铁杆,在创建对象和分配给单例之间放置一个 [storestore] 屏障以及在读取端放置一个 [loadload] 屏障就足够了,并确保使用VarHandle 与单例不透明。

但这远远超出了大多数工程师的理解范围,并且在大多数情况下不会对性能产生太大影响。

如果你想检查某些东西是否会损坏,请查看 JCStress:

https://github.com/openjdk/jcstress

这是一个很棒的工具,可以帮助您证明您的代码已损坏。

【讨论】:

  • 在原始程序和您的第一个未重新排序的示例(引入tmp)中,singleton 被分配了一个完全构造的对象。在您重新排序的示例中,它没有。即使从单线程的角度来看,这也会破坏赋值语义。这是编译器和 CPU 都不允许做的事情。
  • 从单线程的角度来看没有区别,因为线程没有看到它发生。
  • 你能解释一下“线程没有看到它发生”是什么意思吗?因为如果允许您的重新排序示例,即使在完成分配的线程中,分配也会被破坏。那会很糟糕。
  • new 运算符保证返回一个完全构造的对象。原始程序将该构造值分配给变量。在使用该变量之前,该变量必须处于语言语义所期望的状态。同步块将变量提交到主内存:语言语义要求它处于完全构造的状态。如果您将变量用作函数结果,则该函数需要返回一个完全构造的变量。您的重新排序打破了这一切。因此,这是不允许的。
  • @Eugene 我同意(我删除了我之前的回复),学习或纠正你以前学到的东西永远不会太晚。可惜好的答案得分相对较低,维基百科页面也不是很好!
【解决方案2】:

如何证明它一定需要“volatile”?

一般来说,您无法通过测试来证明多线程应用程序的正确性。您可能能够证明不正确,但即使这样也不能保证。正如你所观察的那样。

您没有成功让您的申请失败这一事实并不能证明它是正确的。

证明正确性的方法是在分析之前进行正式(即数学)

很简单地表明,当singleton 不是volatile 时,有一些执行,其中缺少发生在之前。这可能导致不正确的结果,例如多次发生初始化。但不保证你会得到错误的结果。

另一方面是,如果使用 volatile发生在 关系结合代码的逻辑足以构建一个形式(数学)证明,你将永远得到正确的结果。


(我不打算在这里构建证明。这太费力了。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-10
    • 2011-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多