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

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

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

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

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

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

  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认为,许多人会同意他的意见,即使没有明确提及它,仍然意味着时间排序。 因此,我提出了我的第二个程序,它可以certificate这可能的确切程度。

 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. 运行时尽最大努力及时发布写入的软承诺

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

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

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

(如果这让你感到困扰,那么从更基本的意义上考虑,并不能保证JVM能够及时实际执行任何字节码。一个简单地永久停滞的JVM几乎肯定是合法的,因为它基本上不可能提供执行时的硬时间保证。)

请参阅本节(17.4.4) 。 你把规格扭曲了一下,这让你感到困惑。 volatile变量的读/写规范没有说明具体的值 ,具体来说:

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

更新:

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

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

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

顺序一致性是对程序执行中的可见性和排序的有力保证。 在顺序一致的执行中,对所有单个动作(例如读取和写入)的总命令与程序的顺序一致,并且每个单独的动作是primefaces的并且对于每个线程立即可见。 (重点补充)

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

  1. 根据第17.4.5节,“事先发生的关系定义了何时发生数据竞争”。 它似乎没有明确说明,但我认为这意味着如果一个动作发生 – 在另一个动作a’之前发生,它们之间就没有数据竞争。
  2. 根据17.4.3:“如果……变量v的每个读取r看到写入w写入v的值使得w在执行顺序中出现在r之前,则一组动作是顺序一致的…如果a程序没有数据竞赛,那么程序的所有执行都会显得顺序一致。“

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

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

但实际上,表现出这些方式的JVM实现不会被任何接受。


实际上它不是,请参阅我在答案中引用的规范。

哦,是的!

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

我认为Java中的volatile表示为“如果你看到A你也会看到B”。

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

当然,如果你永远不会看到A,你也不能保证看到B. 如果你在B bar看到B,那就说明了A在foo的可见性。 此外,不保证在写入A到foo的线程和在foo看到A的另一个线程之间经过的时间。