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

Mar*_*nik 21 java volatile java-memory-model

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

  1. 写入线程的每个动作在w其写入值之前isharedVar程序顺序happens-before写入动作;
  2. 的值写入i通过w happens-before的成功读取isharedVar由读取线程r;
  3. 成功读取isharedVar由读线程r happens-before的所有后续行动r的程序顺序.

然而,仍有给出没有挂钟时间的保证,当读线程将观察值i.一个完全不会让读取线程看到该值的实现仍然符合此契约.

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

Mar*_*nik 9

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

  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);}
}
Run Code Online (Sandbox Code Playgroud)

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

Thread A wrote 1 at: [0, 2, 5, 7, 9]
Thread B read 0 at: [0, 2, 5, 7, 9]
Run Code Online (Sandbox Code Playgroud)

请注意,我不依赖任何有故障的报告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));
}
Run Code Online (Sandbox Code Playgroud)

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

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]
Run Code Online (Sandbox Code Playgroud)

另一方面,你永远不会期望看到这样的东西,但它仍然是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]
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

清除不稳定的概念

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

  1. 对它的行动的硬保证将尊重发生前的订单;
  2. 运行商为及时发布写入所做的最大努力的软承诺.

注意,第2点不是由JLS以任何方式指定的,它只是出于一般期望而产生的.显然,违背承诺的实施仍然是合规的.随着时间的推移,当我们转向大规模并行架构时,这种承诺可能确实非常灵活.因此,我希望将来保证与承诺的合并将证明是不够的:根据要求,我们需要一个没有另一个,一个与另一个不同,或任何数量的其他组合.

  • @StephenC你没有反驳任何事情; 你只是**声明**必须有一个时间顺序.你没有为你的主张提供一点证据:你得到的最接近的是提供了一个错误的JMM*行动*与物理*事件*的类比,我已经多次反驳和改写了多次反驳.如果我现在同意你的看法,那么我只能盲目相信你的话. (2认同)