【问题标题】:how to understand volatile example in Java Language Specification?如何理解 Java 语言规范中的 volatile 示例?
【发布时间】:2023-07-15 08:27:02
【问题描述】:

我认为 Java 规范中 volatile 的例子有点不对。

在 8.3.1.4 中。易变的领域,它说

class Test {
    static int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

...然后方法二偶尔会打印出大于 i 值的 j 值,因为该示例不包括同步,并且根据第 17.4 节中解释的规则,i 和 j 的共享值可能是乱序更新。

我认为即使这些更新是有序的,方法二仍然可能看到 j 大于 i,因为 System.out.println("i=" + i + " j=" + j) 不是原子的,并且 i 在 j 之前被读取。

方法二同理

read i
read j

所以有可能

read i
i++
j++
read j

在这种情况下,方法二看到 j 的值大于 i,但更新不会乱序。

所以乱序并不是看到 j > i 的唯一原因

应该是System.out.println("j=" + j + " i=" + i);吗?

这次乱序是唯一看到j>i的原因

【问题讨论】:

  • 你的 volatile 变量在哪里?易失性也是存储在主存储器中的一种,所有进程都直接访问它们。对于静态,有一个共享给所有进程的副本
  • 不明白你的问题,但 JLS 的解释对我来说是正确的。
  • @RobbyCornelissen 更新了我的问题
  • @Vipul 这与易失性无关......
  • @curiousguy 进程在这里线程

标签: java jvm language-lawyer java-memory-model


【解决方案1】:

这些例子不仅仅是“有点错误”。

首先,您是对的,即使没有重新排序,j 在此示例中也可能看起来大于 i。这甚至在the same example 后面得到了承认:

另一种方法是将ij 声明为volatile

class Test {
    static volatile int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

这允许方法 one 和方法 two 并发执行,但保证访问 ij 的共享值的次数和顺序完全相同,因为它们似乎在每个线程执行程序文本期间发生。因此,j 的共享值永远不会大于i 的共享值,因为对i 的每次更新都必须在j 的更新发生之前反映在i 的共享值中。然而,任何给定的方法two 的调用都可能观察到j 的值远大于i 观察到的值,因为方法one 可能会在之间执行多次方法two获取i的值以及方法two获取j的值的时刻。

当然,说“j 的共享值永远不会大于 i 的共享值”是很深奥的,只是在下一句“It有可能...... [to] 观察到的 j 的值远大于观察到的 i 的值”。

所以j 永远不会大于i,除非观察到它 大于i?是不是应该说“大一点”是不可能的?

当然不是。这种说法毫无意义,似乎是试图将一些客观事实(如“共享值”)与“观察值”分开的结果,而实际上,程序中只有可观察到的行为。

这用错误的句子来说明:

这允许方法一和方法二同时执行,但保证访问ij 的共享值的次数和顺序完全相同,就像它们在每个线程执行程序文本。

即使有volatile 变量,也没有这样的保证。 JVM 必须保证,观察到的行为 不与规范相矛盾,因此,例如,当您在循环中调用 one() 千次时,优化器仍可能将其替换为以千为单位的原子增量,如果它可以排除另一个线程见证这种优化存在的可能性(除了从更高的速度推断)。

或者换句话说,一个变量(分别是它的内存位置)被实际访问了多少次,是不可观察的,因此没有指定。反正也没关系。对应用程序员而言,重要的是j 可以大于i,无论变量是否声明为volatile

two() 中交换ij 的读取顺序可能会成为一个更好的例子,但我认为,如果JLS §8.3.1.2 不尝试解释其含义,那将是最好的volatile 口语化的,但只是说它根据the memory model 强加了特殊语义,并将其留给 JMM 以正式正确的方式解释。

程序员不应该仅仅通过阅读 8.3.1.4. 来掌握并发性,所以这个例子在这里毫无意义(在最好的情况下;最坏的情况会让人觉得这个例子足以理解这个问题)。

【讨论】:

  • 我很惊讶 JLS 会有如此误导性的句子
  • @user7 还有很多需要修改的地方。 JLS 是由人类编写的……
【解决方案2】:

Holger 在他的回答中所说的是绝对正确的(再次阅读并接受它),我只想补充一点,使用jcstress,这甚至很容易证明。测试本身只是对Coherence Sample 的一个小重构(这太棒了!IMO):

import org.openjdk.jcstress.annotations.Actor;
import org.openjdk.jcstress.annotations.Expect;
import org.openjdk.jcstress.annotations.JCStressTest;
import org.openjdk.jcstress.annotations.Outcome;
import org.openjdk.jcstress.annotations.State;
import org.openjdk.jcstress.infra.results.II_Result;

@JCStressTest
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE_INTERESTING, desc = "only j updated")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "only i updated")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "both updates lost")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "both updated")
@State
public class SOExample {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    @Actor
    public void writeActor() {
        ++h1.i;
        ++h1.j;

    }

    @Actor
    public void readActor(II_Result result) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;

        h1.trap = 0;
        h2.trap = 0;

        result.r1 = h1.i;
        result.r2 = h2.j;
    }

    static class Holder {

        int i = 0;
        int j = 0;

        int trap;
    }

}

即使你不理解代码,关键是运行它会将ACCEPTABLE_INTERESTING 显示为绝对可能的结果;有volatile int i = 0; volatile int j = 0; 或没有volatile

【讨论】: