知名度的挥发性关于时效的详细语义(Detailed semantics of volatile re

2019-06-17 14:51发布

考虑一个volatile int sharedVar 。 我们知道,JLS给了我们以下保证:

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

然而,仍有给出何时读线程将观察值没有挂钟时间保证i 。 简单地不让读线程看到价值的实现仍与本合同的规定。

我曾想过这一段时间,我看不到任何的漏洞,但我相信一定有。 请指出漏洞,我的推理。

Answer 1:

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

  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认为,许多人可能会同意他的观点,即之前发生 ,虽然没有明确提到这一点,仍然意味着时间排序。 因此,我提出我的演示的确切程度,这可能是这样的第二程序。

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 。

一个corrollary这个规则是一个线程只写的这些行动,绝不读取任何东西可以无限期地在不违反之前发生关系被推迟

清理挥发概念

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

  1. 硬保证它的行动将尊重之前发生排序;
  2. 对写的及时发布运行时的尽力而为的软承诺

注意点2不是由JLS以任何方式指定,它只是一种被普遍期望就出现了。 打破承诺的实现依然符合规定,很明显。 随着时间的推移,随着我们向大规模并行架构,这一承诺可能确实被证明是相当灵活。 因此,我预计,在未来的承诺保证混为一谈将被证明是不够的:根据要求,我们需要一个没有其他的,一个具有不同的风味其他的,或任意数量的其他组合。



Answer 2:

你是部分正确。 我的理解是,这将是合法的,虽然当且仅当线程r没有在一个之前发生关系相对于螺纹有任何其他操作搞w

所以,有没有在挂钟时间条件的担保; 但在方案中的其他同步点方面的保证。

(如果这困扰你,认为在一个更根本的意义上来说,不能保证JVM将永远实际上及时执行任何字节码。简单地永远停滞不前一个JVM几乎肯定会是合法的,因为它本质上是无法提供上执行的硬定时保证。)



Answer 3:

请参阅本节(17.4.4) 。 你已经扭曲规范了一下,这是什么是混淆你。 对于volatile变量的读/写规范只字未提特定值 ,具体如下:

  • 于挥发性变量(§8.3.1.4)V的写入同步,与所有后续的通过任何线程读取的v的(其中,随后根据该同步命令被定义)。

更新:

作为@AndrzejDoyle提到,你可以想见,纷纷跟帖r读取过时的值,只要没有别的线程做点建立与线程的同步点之后w在执行过程中的一些后来点(如,那么你将是违反规格)。 所以,是的,有一定的回旋余地,但螺纹r会在什么可以做(例如,写入到System.out将建立以后的同步点作为最上游impls同步)进行非常严格的限制



Answer 4:

我不相信任何以下的了。 这一切都归结到未定义除了两个在17.4.4,在那里它tautologically“根据该同步命令定义的”提到的含义是“后续”。)

我们真的有去的唯一的事情是在第17.4.3:

顺序一致性是在程序的执行制作约知名度和排序非常有力的保障。 在一个顺序一致的执行,存在于所有个人行动共顺序(如读取和写入),这是程序的顺序一致,且每个人的行动是原子,并立即可见的每一个线程。 (强调)

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

  1. 根据第17.4.5,“在之前发生关系定义当数据争发生。” 它似乎并没有被明确说明,但我认为,这意味着,如果一个动作之前发生的另一个动作”,它们之间没有数据的比赛。
  2. 据17.4.3:“A组动作顺序是一致的,如果...每个读取变量v [R看到通过写书面W的值到v的W在执行顺序是:R之前,...如果程序没有数据竞争,那么该程序的所有执行会显得顺序是一致的。”

如果你写一个volatile变量v和它在另一个线程随后读取,这意味着,在写之前发生的读。 这意味着,存在写入和读出,这意味着它们必须是一致的顺序之间没有数据的比赛。 这意味着读R必须看到由写瓦特 (或随后的写入)写入的值。



Answer 5:

这里并不需要是一个漏洞。 这的确是理论上的“法律”来实现一个JVM是这样做。 同样的,这在理论上是“合法”永不调度线程,其名称与一个开始"X" 。 或实现永不运行GC一个JVM。

但在实践中,这些行为方式的JVM实现不会发现任何验收。


其实不是的话,请参阅我在回答中引用的规范。

哦,是的,它是!

在读永久阻塞线程的实现将与JLS 17.4.4符合技术要求。 在“后续读”永远不会完成。



Answer 6:

我认为, volatile在java中的术语“如果你看到一个你还会看到B”表示。

为了更加明确,Java的承诺,当你线程读取volatile变量foo ,并认为价值A,你有一些保证,而当你以后在同一线程读取其他变量,你会看到什么。 如果写了同一线程foo也写了B到bar (书面之前foo ),你保证至少看到的B bar

当然,如果你从来没有看到A,你不能保证见B两种。 如果你看到的B bar ,那也不说A的知名度foo 。 此外,该线程书写的之间的时间到foo ,而另一个线程看到在foo不能保证。



文章来源: Detailed semantics of volatile regarding timeliness of visibility