Java内存屏障

3
我正在阅读《JSR 133 Cookbook》,关于内存屏障有以下问题需要解答。书中提供了插入内存屏障的例子,但仅涉及本地变量的读写。假设我有以下变量:
int a;
volatile int b;

而代码

b=a;

我理解得没错,这一行代码将生成以下指令。
load a
LoadStore membar
store b

据我所知,a不参与happens-before。所以我认为不是这样的。 - user166390
1
JMM 给出了两个保证:对 b 的写入将会被任何后续的 b 读取可见,并且任何先前对 a 的写入也将可见。 - assylias
我同意,这是JMM的保证。但我对特定的内存屏障很感兴趣。实际上,内存屏障比happens-before边缘更容易理解。例如,int a = 0; volatile int b = 0,c = 1; b = 1; a = c; 在线程2中://是否可能在这里看到a = 1; b = 0?根据happens before行为,指令可以重新排序,因为它不是相同的volatile变量的读写。但根据JMM cookbook的说法,由于内存屏障,它们不会被重新排序。 - andrershov
@andrershov 事实上,最终JVM应该符合JMM的规定,而JMM不会考虑内存屏障(这是一种实现细节)。在您的示例中,如果您在线程2中读取a之前先读取b,那么它可能具有任何值,因为在线程2中读取a和在线程1中写入a之间不存在happens-before关系。一旦您在线程2中读取了b,重排序就不再可能。 - assylias
1个回答

1
JVM 的底层行为仅针对 volatile 变量进行保证。即使在一个线程完成对 b = a; 语句的评估后,两个单独的线程可能会访问变量 'a' 的不同值。JVM 仅保证对 volatile 变量的访问是序列化的,并具有 Happens-Before 语义。这意味着在两个不同的线程中执行 b = a; 的结果(在 "volatile" 值为 'a' 的情况下)是不确定的,因为 JVM 只表示对 'b' 的存储是序列化的,它没有保证哪个线程具有优先权。
更准确地说,这意味着 JVM 将变量 'b' 视为具有自己的锁;每次只允许一个线程读取或写入 'b';而此锁仅保护对 'b' 的访问,而不保护其他任何内容。

现在,这在不同的JVM下有不同的含义,在不同机器架构上实现此锁定可能会导致应用程序运行时行为迥异。您可以信任的唯一保证是Java参考手册所说的,“一个字段可以被声明为volatile,此时Java内存模型确保所有线程都看到一个变量的一致值。”如果需要进一步了解,请参阅Dennis Byrne出色的article,以了解不同JVM实现如何处理此问题。

发生之前的语义在提供的示例中并不是非常有趣,因为整数原语并没有提供很多机会进行指令重排序,而volatile部分旨在弥补这种情况。一个更好的例子是这个:

private AnObjectWithAComplicatedConstructor _sampleA;
private volatile AnObjectWithAComplicatedConstructor _sampleB;

public void getSampleA() {
    if (_sampleA == null) {
        _sampleA = new AnObjectWithAComplicatedConstructor();
    }
    return _sampleA;
}

public void getSampleB() {
    if (_sampleB == null) {
        _sampleB = new AnObjectWithAComplicatedConstructor();
    }
    return _sampleB;
}

在这个例子中,'_sampleA'字段存在一个严重的问题;在多线程情况下,'_sampleA'可能正在一个线程中被初始化的同时,另一个线程尝试使用它,导致各种零散和非常难以复制的错误。为了看到这一点,请考虑线程X执行getSampleA()中的new字节码指令语句,然后将(尚未初始化)结果存储在字段'_sampleA'中。现在JVM暂停线程X,线程Y开始执行getSampleA()并看到'_sampleA'不为空;然后返回未初始化的值,并且线程Y现在开始调用生成的实例上的方法,导致各种问题;当然,这些问题只会在生产环境中,在奇怪的时间和重负载下出现。
对于字段_sampleB的最坏情况是它可能有多个线程初始化单个实例;其中除一个之外的所有实例最终都将被丢弃。像这样的代码应该包装在“synchronized”块中,但是volatile关键字也可以解决问题,因为它要求最终存储在'_sampleB'中的值具有Happens-Before语义,这意味着等号右侧的内容在执行等号左侧的内容时已经完成。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接