易失性变量和非易失性重新排序/可见性

Posted

技术标签:

【中文标题】易失性变量和非易失性重新排序/可见性【英文标题】:Volatile variable and non volatile reordering / visibility 【发布时间】:2016-02-02 00:27:22 【问题描述】:

所以我认为我对这些东西足够了解,直到我读到一些让我怀疑我对这个主题的知识的东西。我几乎可以肯定这本书是不正确的,但我也想问问社区。​​p>

PS:没有看过本书的勘误表,所以很可能被披露为错误。

一个简化的例子:

public class VolatileMain 

private volatile int a = 0;
private String text = "";

public static void main(String[] args) throws Exception 

    VolatileMain vm = new VolatileMain();

    Thread writer = new Thread() 

        @Override
        public void run() 
            System.out.println("Running thread " + Thread.currentThread().getName());
            vm.text = "hello world";
            vm.a = 5;
        
    ;

    writer.start();
    writer.join();

    System.out.println("Running thread " + Thread.currentThread().getName());
    System.out.println(vm.a);
    System.out.println(vm.text);

   


因此,鉴于示例,假设线程编写器对“文本”的写入保证对读取它的任何其他线程可见是正确的吗?

似乎作者在捎带变量“a”的易失语义,并确保在刷新“a”时也会刷新对“text”的写入,这是保证吗?

我不认为是,但我自己的快速测试(上图)恰恰相反

你的想法。

【问题讨论】:

这个例子很愚蠢,因为join 无论如何都是一个同步点,因此在这种情况下非易失性应该可以正常工作。 @the8472 确实,提交帖子后我的脑海中闪过这个想法,但是我怀疑回答的人很好理解了这个问题的意图,但感谢您指出 【参考方案1】:

不,不能保证,因为“冲洗”并不是那么简单。即使您实际上将非易失性内容写入“主存储器”,也不能保证其他线程中的后续读取将从该主存储器中读取它。考虑以下示例:

public class VolatileMain 

    private volatile int a = 0;
    private String text = "";

    public static void main(String[] args) throws Exception 

        VolatileMain vm = new VolatileMain();

        Thread writer = new Thread() 

            @Override
            public void run() 
                // Added sleep here, so waitForText method has chance to JIT-compile
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                System.out.println("Running thread " + Thread.currentThread().getName());
                vm.text = "hello world";
                vm.a = 5;
                System.out.println("Text changed!");
            
        ;

        writer.start();

        waitForText(vm);

        writer.join();

        System.out.println("Running thread " + Thread.currentThread().getName());
        System.out.println(vm.a);
        System.out.println(vm.text);

    

    // Wait for text change in the spin-loop
    private static void waitForText(VolatileMain vm) 
        int i = 0;
    /*
        @Edit by Soner
        Compiler may do following steps to optimize in lieu.
        String myCache = vm.text;
        -- Assume that here myCache is "" -- so stay forever.
        while (myCache.equals(""))  i++; 
    */
        while (vm.text.equals("")) 
            i++;
        
        System.out.println("Wait complete: " + i);
    

waitForText 很有可能永远不会完成,因为 JIT 编译器会优化它并将 vm.text 的读取移出循环(因为它不是易失性的,因此在循环和text 在循环内永远不会改变)使循环无限。

Volatile read/write 不仅会影响内存提交,还会改变 JIT 编译策略。在while循环中添加vm.a的读取,程序将正常运行。

【讨论】:

【参考方案2】:

假设线程编写器对“文本”的写入保证对任何其他读取它的线程可见是正确的吗?

没有。但它保证在读取text 之前被读取a 的任何其他线程可见,就像您的示例一样:

text 的写入发生在写入线程中a 的写入之前 a 在 writer 中的写入发生在 a 在主线程中的读取之前 happens-before 关系是可传递的 因此text 的写入发生在a 的读取之前。

【讨论】:

完全正确。正是a 的读数在两个线程之间建立了正确的(之前发生的)关系。当您考虑可见性如何在 Java 中的多线程中工作时,您不应该考虑诸如“刷新”之类的事情。还有更多的因素在起作用,即使它在您的硬件上以某种方式工作,它在其他硬件上的 JVM 中的工作方式也可能大不相同。只需阅读 Java 内存模型规范(Java 语言规范的一部分)docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4 @ErwinBolwidt - 当您考虑可见性时,您不应该考虑诸如“冲洗”之类的事情。我不确定这是否正确。 JVM 通过实际发送/创建内存屏障指令来确保 Happens-before。当处理器执行这些指令时,它(几乎总是)将本地缓存刷新到主内存中。在某种程度上,刷新与之前发生的事情(内存障碍)直接相关 @VinodMadyalkar 这完全是一个实现细节。 JVM 是跨平台的,它唯一需要遵守的是 JLS 和 JVM 规范。如果您尝试考虑底层硬件,您将遇到问题,因为 Java/JVM 可能会再存在 30 年,届时我们将拥有您现在无法想象的硬件架构。 Java 内存模型甚至一次都没有提到刷新。 @ErwinBolwidt - 是的。不同的平台可能有不同的处理内存屏障的方法。我知道 JVM 规范没有提到 之前应该如何实现。但是在大多数现代平台上,将缓存刷新到主内存是处理内存屏障的方式:) @VinodMadyalkar 弱排序的内存架构(例如 ARM)可能会在不对所有存储进行排序的情况下,对单个写入进行排序,并提供一些相对于它们的数据依赖性的排序保证,即不刷新管道的写入缓冲区。而且我认为 POWER 甚至可以做一些无视数​​据依赖性的疯狂事情。不要在 x86 语义中推理事物,它可能在其他平台上失败。

以上是关于易失性变量和非易失性重新排序/可见性的主要内容,如果未能解决你的问题,请参考以下文章

将易失性数组与非易失性数组进行比较

将易失性数组转换为非易失性数组

优化volatile变量

everspin非易失性存储器中MRAM的潜在用途

展望由非易失性设备构成的未来存储

MicroPython ESP32NVS数据非易失性存储示例讲解说明