关于可见性及时性的易失性的详细语义

时间:2012-08-01 14:34:35

标签: java volatile java-memory-model

考虑volatile int sharedVar。我们知道JLS为我们提供了以下保证:

  1. 写作线程w的每个操作,在程序顺序i中写入值sharedVarhappens-before之前写入操作;
  2. i w通过阅读帖happens-beforei成功阅读sharedVar后写入值r;
  3. 阅读主题i sharedVar r成功阅读happens-before r所有i的后续行动。
  4. 然而,对于 读取线程将观察值{{1}},仍然没有挂钟时间保证。只需永远不会的实现让阅读线程看到该值仍然符合此合同。

    我已经考虑了一段时间,我看不到任何漏洞,但我认为必须有。请指出我的推理中的漏洞。

6 个答案:

答案 0 :(得分:9)

事实证明,答案和随后的讨论只能巩固我原来的推理。我现在有一些证明的方式:

  1. 采用在写入线程开始执行之前读取线程完全执行的情况;
  2. 注意此特定运行创建的同步顺序;
  3. 现在在挂钟时间内移动线程,以便它们并行执行,但保持相同的同步顺序
  4. 由于Java Memory Model没有提及挂钟时间,因此不会有障碍。现在,您有两个与读取线程并行执行的线程,观察写入线程没有执行任何操作。 QED。

    示例1:一个写作,一个阅读线程

    为了使这个发现极为尖锐和真实,请考虑以下程序:

    static volatile int sharedVar;
    
    public static void main(String[] args) throws Exception {
      final long startTime = System.currentTimeMillis();
      final long[] aTimes = new long[5], bTimes = new long[5];
      final Thread
        a = new Thread() { public void run() {
          for (int i = 0; i < 5; i++) {
            sharedVar = 1;
            aTimes[i] = System.currentTimeMillis()-startTime;
            briefPause();
          }
        }},
        b = new Thread() { public void run() {
          for (int i = 0; i < 5; i++) {
            bTimes[i] = sharedVar == 0?
                System.currentTimeMillis()-startTime : -1;
            briefPause();
          }
        }};
      a.start(); b.start();
      a.join(); b.join();
      System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes));
      System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes));
    }
    static void briefPause() {
      try { Thread.sleep(3); }
      catch (InterruptedException e) {throw new RuntimeException(e);}
    }
    

    就JLS而言,这是一个合法的输出:

    Thread A wrote 1 at: [0, 2, 5, 7, 9]
    Thread B read 0 at: [0, 2, 5, 7, 9]
    

    请注意,我不依赖于currentTimeMillis的任何有问题的报告。报道的时间是真实的。但是,实现确实选择只在读取线程的所有操作之后才能使写入线程的所有操作都可见。

    示例2:两个读取和写入的线程

    现在@StephenC认为,很多人会同意他的意见,发生在之前,即使没有明确提及它,仍然暗示时间排序。因此,我提出了我的第二个程序,它可以证明这可能的确切程度。

    public static void main(String[] args) throws Exception {
      final long startTime = System.currentTimeMillis();
      final long[] aTimes = new long[5], bTimes = new long[5];
      final int[] aVals = new int[5], bVals = new int[5];
      final Thread
        a = new Thread() { public void run() {
          for (int i = 0; i < 5; i++) {
            aVals[i] = sharedVar++;
            aTimes[i] = System.currentTimeMillis()-startTime;
            briefPause();
          }
        }},
        b = new Thread() { public void run() {
          for (int i = 0; i < 5; i++) {
            bVals[i] = sharedVar++;
            bTimes[i] = System.currentTimeMillis()-startTime;
            briefPause();
          }
        }};
      a.start(); b.start();
      a.join(); b.join();
      System.out.format("Thread A read %s at %s\n",
          Arrays.toString(aVals), Arrays.toString(aTimes));
      System.out.format("Thread B read %s at %s\n",
          Arrays.toString(bVals), Arrays.toString(bTimes));
    }
    

    为了帮助理解代码,这将是一个典型的,现实世界的结果:

    Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14]
    Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]
    

    另一方面,你永远不会期望看到这样的东西,但是它仍然是JMM标准的合法性

    Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14]
    Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]
    

    JVM实际上必须预测线程A将在时间14写入什么才能知道什么让线程B在时间1读取。这种可信性甚至可行性是非常可疑的

    由此我们可以定义JVM实现可以采用的以下现实自由:

    线程中任何不间断的 release 操作序列的可见性可以安全地推迟到获取操作中断之前。

    术语发布获取JLS §17.4.4中定义。

    此规则的一个原因是,只有写入且永远不会读取任何内容的线程的操作可以无限期推迟,而不会违反发生之前的关系。

    清除易变的概念

    volatile修饰符实际上是两个截然不同的概念:

    1. 难以保证对其执行的操作将尊重发生在之前的订单;
    2. 运行时努力及时发布写入的软承诺
    3. 注意,JLS没有以任何方式指定第2点,它只是出于一般期望而产生的。显然,违背承诺的实施仍然是合规的。随着时间的推移,当我们转向大规模并行架构时,这种承诺可能确实非常灵活。因此,我希望将来保证与承诺的混合证明是不够的:根据要求,我们需要一个没有另一个,一个与另一个不同,或任何数量的其他组合。

答案 1 :(得分:4)

你是部分正确的。我的理解是,这是合法的,当且仅当线程r没有参与任何其他具有相对于线程w的事先关系的操作时。

因此,就挂钟时间而言, 时无法保证;但是在程序中的其他同步点方面有保证。

(如果这让你烦恼,那么从更基本的意义上考虑,不能保证JVM能够及时实际执行任何字节码。一个简单地停滞不前的JVM几乎会当然是合法的,因为在执行时提供硬时间保证基本上是不可能的。)

答案 2 :(得分:3)

请参阅this section (17.4.4)。你把规格扭曲了一下,这让你感到困惑。 volatile变量的读/写规范没有提及特定值,特别是:

  • 对volatile变量(第8.3.1.4节)的写入v与任何线程的v的所有后续读取同步(其中后续是根据同步顺序定义的)。

更新:

正如@AndrzejDoyle所提到的那样,你可以想象线程r读取一个陈旧的值,只要没有其他该线程在该点之后与线程{{1}建立同步点在执行的某个稍后阶段(因为那样你就违反了规范)。所以,是的,那里有一些摆动空间,但是线程w非常受限它可以做什么(例如,写入System.out将建立一个稍后的同步点,因为大多数流impls是同步的。)

答案 3 :(得分:2)

我不相信下面的任何一个。这一切都归结为“后续”的含义,除了17.4.4中的两个提及之外,它是未定义的,其中它是“按照同步顺序定义”的同义词。)

我们唯一要做的就是第17.4.3节:

  

顺序一致性是对程序执行中的可见性和排序的有力保证。在顺序一致的执行中,所有单个操作(例如读取和写入)的总顺序与程序的顺序一致,并且每个单独的操作都是原子的,并且每个线程都可以立即看到。 / strong>(强调补充)

我认为有这样的实时保证,但你必须从JLS chapter 17的各个部分拼凑起来。

  1. 根据第17.4.5节,“事先发生的关系定义了何时发生数据竞争”。它似乎没有明确说明,但我认为这意味着如果一个动作 a 发生在另一个动作 a'之前,它们之间就没有数据竞争
  2. 根据17.4.3:“如果......变量 v 的每个读取 r 看到写入的值<,则一组动作是顺序一致的< em> w 到 v ,使得w在执行顺序中出现在r之前......如果程序没有数据竞争,那么程序的所有执行将看起来是顺序一致的。 “
  3. 如果您写入易失性变量v并随后在另一个线程中读取它,则意味着写入发生在读取之前。这意味着写入和读取之间没有数据竞争,这意味着它们必须是顺序一致的。这意味着读取 r 必须看到写入 w (或后续写入)写入的值。

答案 4 :(得分:1)

不需要有漏洞。实际上,实现这一点的JVM在理论上是“合法的”。同样,从不调度名称以"X"开头的线程在理论上是“合法的”。或者实现一个从不运行GC的JVM。

但在实践中,表现出这些方式的JVM实现不会被接受。


  

实际上并非如此,请参阅我在答案中引用的规范。

哦,是的!是的。

在读取中永久阻止线程的实现在技术上符合JLS 17.4.4。 “后续阅读”永远不会完成。

答案 5 :(得分:1)

我认为Java中的volatile用&#34表示;如果你看到A,你也会看到B&#34;。

为了更明确,Java承诺当你线程读取一个volatile变量foo并看到值A时,你可以保证稍后在同一个线程上读取其他变量时会看到什么。如果向foo写入A的同一个帖子也将B写入bar(在将A写入foo之前),则保证在bar中至少看到B }。

当然,如果你从未见过A,你也无法保证看到B.如果你在bar中看到B,则说明foo中A的可见性。此外,无法保证在写入A到foo的线程与在foo中看到A的另一个线程之间经过的时间。