关于可见性及时性的变化的详细语义清除易失性概念

2022-09-01 23:37:00

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

  1. 写入线程的每个操作,在其写入值之前,在程序中按写入操作的顺序;wisharedVarhappens-before
  2. 通过读取线程成功读取值来写入值iwhappens-beforeisharedVarr;
  3. 通过读取线程按程序顺序成功读取所有后续操作。isharedVarrhappens-beforer

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

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


答案 1

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

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

由于Java内存模型没有引用挂钟时间,因此不会有障碍。现在,您有两个线程与读取线程并行执行,观察写入线程未执行任何操作。新浪网.

示例 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 处将写入的内容,以便知道在时间 1 时让线程 B 读取的内容。这样做的合理性甚至可行性都相当值得怀疑。

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

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

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

此规则的一个保证是,只写入从不读取任何内容的线程的操作可以无限期地推迟,而不会违反“发生前”关系。

清除易失性概念

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

  1. 硬保证对其采取的行动将尊重订单前发生;
  2. 运行时尽最大努力及时发布写入的软承诺

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


答案 2

你部分是对的。我的理解是,当且仅当线程不参与与线程具有先前发生关系的任何其他操作时,这将是合法的。rw

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

(如果这困扰你,请考虑在更基本的意义上,不能保证JVM会及时实际执行任何字节码。一个永远停滞不前的JVM几乎肯定是合法的,因为基本上不可能为执行提供硬定时保证。


推荐