易失性变量和非易失性重新排序/可见性
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 语义中推理事物,它可能在其他平台上失败。以上是关于易失性变量和非易失性重新排序/可见性的主要内容,如果未能解决你的问题,请参考以下文章