23

考虑一个volatile int sharedVar. 我们知道 JLS 为我们提供以下保证:

  1. 写入线程的每个操作在w其写入值之前isharedVar程序顺序happens-before写入操作;
  2. i通过读取线程w happens-before成功读取来i写入值;sharedVarr
  3. 读取线程成功读取i程序顺序中的所有后续操作。sharedVarr happens-beforer

但是,对于读取线程何时会观察到该值仍然没有给出挂钟时间保证i。一个根本不会让读取线程看到该值的实现仍然符合此合同。

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

4

5 回答 5

10

事实证明,答案和随后的讨论只是巩固了我最初的推理。我现在有一些东西可以证明:

  1. 以读线程在写线程开始执行之前完全执行的情况为例;
  2. 请注意此特定运行创建的同步顺序;
  3. 现在在挂钟时间移动线程,以便它们并行执行,但保持相同的同步顺序

由于 Java 内存模型没有参考挂钟时间,因此不会有任何障碍。您现在有两个线程与读取线程并行执行,观察到写入线程没有执行任何操作。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 争辩说,许多人会同意他的观点,即使没有明确提及, happens-before仍然意味着时间排序。因此,我提出了我的第二个程序,它展示了这种情况的确切程度。

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 实现可以采取的以下现实自由:

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

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

这条规则的一个推论是,一个只写而从不读任何东西的线程的动作可以无限期地推迟,而不会违反之前发生的关系。

清除易变概念

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

  1. 硬保证对它的操作将尊重发生前的顺序;
  2. 运行时尽最大努力及时发布写入的软承诺。

请注意,JLS 并未以任何方式指定第 2 点,它只是出于普遍预期。显然,违背承诺的实现仍然是合规的。随着时间的推移,随着我们转向大规模并行架构,这一承诺可能确实被证明是相当灵活的。因此,我希望将来将保证与承诺的合并证明是不够的:根据要求,我们将需要一个没有另一个,一个具有不同风格的另一个,或者任何数量的其他组合。

于 2012-08-01T18:52:09.357 回答
4

你是部分正确的。我的理解是,这将是合法的,尽管当且仅当 threadr不参与任何其他与 thread 具有发生前关系的操作时w

所以不能保证挂钟时间是什么时候。但是程序中的其他同步点有保证。

(如果这让您感到困扰,请从更基本的意义上考虑,不能保证 JVM 会真正及时地执行任何字节码。一个永远停止的 JVM 几乎肯定是合法的,因为它基本上不可能提供执行时的硬时间保证。)

于 2012-08-01T14:50:51.577 回答
3

请参阅本节 (17.4.4)。您稍微扭曲了规范,这让您感到困惑。volatile 变量的读/写规范没有说明具体值,特别是:

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

更新:

正如@AndrzejDoyle 所提到的,您可以想象让线程r读取一个陈旧的值,只要该线程在该点之后没有其他任何事情在执行的稍后时间点与线程建立同步点w(因为那样您将违反规范) . 所以是的,那里有一些回旋的空间,但是线程在它可以做的事情上r会受到很大的限制(例如,写入 System.out 会建立一个稍后的同步点,因为大多数流 impl 都是同步的)。

于 2012-08-01T14:44:58.910 回答
3

我不再相信以下任何内容。这一切都归结为“后续”的含义,除了 17.4.4 中的两次提及外,它是未定义的,其中重复地“根据同步顺序定义”。)

我们真正需要做的唯一事情是在第 17.4.3 节中:

顺序一致性是对程序执行中的可见性和顺序的非常强大的保证。在顺序一致的执行中,所有单独的操作(例如读取和写入)都有一个与程序顺序一致的总顺序,并且每个单独的操作都是原子的,并且对每个线程都是立即可见的。(重点补充)

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

  1. 根据第 17.4.5 节,“happens-before 关系定义了何时发生数据竞争”。它似乎没有明确说明,但我认为这意味着如果一个动作a发生在另一个动作a'之前,它们之间就没有数据竞争。
  2. 根据 17.4.3:“一组动作是顺序一致的,如果......变量v的每个读取r看到写入wv的值,使得 w 在执行顺序中位于 r 之前......如果程序没有数据竞争,那么程序的所有执行将看起来是顺序一致的。”

如果您写入 volatile 变量v并随后在另一个线程中读取它,这意味着写入发生在读取之前。这意味着写入和读取之间没有数据竞争,这意味着它们必须是顺序一致的。这意味着读取r必须看到写入w(或后续写入)写入的值。

于 2012-08-17T21:22:52.793 回答
1

我认为volatileJava中的表达方式是“如果你看到A,你也会看到B”。

更明确地说,Java 承诺当您线程读取 volatile 变量foo并看到值 A 时,您可以保证稍后在同一线程上读取其他变量时会看到什么。如果将 A 写入的同一线程foo也将 Bbar写入(在将 A 写入之前foo),那么您可以保证至少在bar.

当然,如果你永远看不到A,也不能保证你能看到B。如果你看到 B in bar,那说明 A in 的可见性没有任何意义foo。此外,不能保证写入 A 的线程foo和看到 A 的另一个线程之间经过的时间。foo

于 2014-12-19T21:49:09.447 回答