如何理解Java语言规范中的易失性示例?

时间:2019-05-24 03:57:27

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

我认为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);
    }
}
  

...然后方法二可能偶尔会打印j的值,该值大于i的值,因为该示例不包含同步,并且在第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

的唯一原因

2 个答案:

答案 0 :(得分:4)

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

首先,您是正确的,即使在没有重新排序的情况下,本示例中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的每次更新都必须反映在i的共享值中{1}}发生。但是,有可能方法j的任何给定调用都可能观察到two的值,该值远大于j观察到的值,因为方法i可能会在方法one获取two的时刻到方法i获取two的时刻之间执行了多次。

当然,可以说“ j的共享价值永远不比j 的共享价值”,只是在下一个句子中说“ 可能[…]观察到的i的值比观察到的j 的值大得多。”

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

当然不是。该语句没有任何意义,似乎是试图将诸如“共享价值”与“观察价值”之类的客观事实区分开来的结果,而实际上,程序中只有可观察到的行为。

这用错误的句子来说明:

  

这允许方法一和方法二同时执行,但是保证对ii的共享值的访问发生的次数和次数完全相同,并且顺序完全相同似乎发生在每个线程执行程序文本期间。

即使具有j变量,也无法保证。 JVM必须保证的是,观察到的行为不会与规范相矛盾,因此,例如,当您在循环中调用volatile千次时,优化器可能仍会替换它如果可以排除另一个线程见证这种优化的可能性(不是从更高的速度中推论出来的话),那么原子数就增加一千。

或者换句话说,变量(实际上是其存储位置)实际被访问的次数是不可观察的,因此未指定。没关系。对于应用程序程序员而言,重要的是,无论变量是否声明为one()j都可以大于i

volatile中交换ij的读取顺序可能是一个更好的示例,但我认为,如果JLS§8.3.1.2做到了,那将是最好的选择并不是试图用口语来解释two()的含义,而是只是说它根据the memory model施加了特殊的语义,并将其留给JMM以形式上正确的方式进行解释。

程序员不应仅通过阅读8.3.1.4来掌握并发性,因此此处的示例毫无意义(最好的情况;最坏的情况是给人以示例足以理解问题的印象)。 / p>

答案 1 :(得分:0)

霍尔格在回答中说的是绝对正确的(再次阅读并接受),我只想用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