Java -> volatile 和 final: Volatile as flushing-all-memory-content

问题描述

看看这里的答案 (1):

https://stackoverflow.com/a/2964277/2182302 (Java Concurrency : Volatile vs final in "cascaded" variables?)

以及我在这里的旧问题 (2):

one java memoryFlushing volatile: A good programdesign?

据我所知(见 (2)),我可以使用 volatile 变量作为所有内存内容的内存屏障/刷新器,而不仅仅是 volatile 关键字引用的内容

现在(1)中接受的答案说它只会刷新附加了 volatile-keyowrd 的内存。

那么现在什么是正确的?如果 (2) 中的全部刷新原则是正确的,为什么我不能将 volatile 附加到与 final 结合的变量?

解决方法

没有一个答案是正确的,因为你的想法是错误的。 '刷新内存'的概念是简单的。它在 Java 虚拟机规范中没有。这不是一件事。是的,许多 CPU/架构确实以这种方式工作,但 JVM 则不然。

您需要按照 JVM 规范进行编程。不这样做意味着您编写的代码每次都可以在您的机器上完美运行,然后您将其上传到您的服务器,但它在那里失败。这是一个可怕的场景:有错误的代码,但是测试无法触发的错误。 Yowza,那些很糟糕。

那么,JVM 规范中的是什么

不是“冲洗”的概念。它所拥有的是 HBHA 的概念:发生在之前/发生在之后。这是它的工作原理:

  1. 有一个特定交互的列表,其中设置了某些代码行被定义为“发生在之前”(HB/HA = 发生在之前/发生在之后)另一行。下面给出了这个列表的概念。
  2. 对于具有 HBHA 关系的任何两条线,HA 线不可能观察到任何状态,以至于 HB 线似乎尚未运行。它基本上是在说:HB 线出现在 HA 线之前,除非不是那么强:你不能观察到相反的情况(即 HB 改变变量 X,HA 线没有看到这种变化到 X,那会观察到相反的,这是不可能的)。除了时间方面。实际上,HB/HA 并不意味着行会更早或更晚执行:如果您有 2 行具有 HB/HA 关系且互不影响的行(一个写入变量 X,另一个读取完全不同的变量 Y) ,一起工作的 JVM/CPU 可以随意重新排序。
  3. 对于没有定义 HB/HA 关系的任意两条线,JVM 和 CPU 可以随意做任何事情。包括无法用简单的“冲洗”模型解释的事情。

例如:

int a = 0,b = 0;

void thread1() {
    a = 10;
    b = 20;
}

void thread2() {
    System.out.println(b);
    System.out.println(a);
}

在上面,线程1修改a/b的状态和线程2读取它们之间没有建立HB/HA关系。

因此,JVM 打印 20 0 是合法的,即使这不能用基本的刷新概念来解释:JVM '刷新' b 但不是合法的一.

您不太可能能够编写此代码并在任何 JVM 版本或任何硬件上实际观察到 20/0 打印,但重点是:这是允许的,并且有一天(或者可能已经存在),JVM+硬件+操作系统版本+机器状态的一些奇特组合结合起来实际实现了这一点,所以如果你的代码在发生这一系列事件时中断,那么你写了一个错误

实际上,如果一行改变 state,另一行读取它,而这两行没有 HB/HA,你搞砸了,你需要修复你的错误。甚至(尤其是!)如果您无法编写一个实际证明它的测试。

这里的技巧是易失性读取确实建立 HB/HA,因为这是 JVMS 规范必须同步内容的唯一机制,是的,这具有保证您'查看所有更改'。但这根本不是一个好主意。特别是因为 JVMS 还说热点编译器可以自由地消除没有副作用的行。

所以现在我们将不得不讨论“建立 HBHA”是否是副作用。可能是这样,但现在我们开始了解优化规则:

编写惯用代码

每当 azul、openjdk 核心开发团队等正在寻求改进热点编译器的大量优化功能时,他们查看现实生活中的代码。这就像一个巨大的模式匹配器:他们在代码中寻找模式并找到优化它们的方法。他们不只是为所有可以想象的事情编写检测器:他们非常喜欢为通常出现在现实生活中的 Java 代码中的模式编写优化器。毕竟,花时间和精力优化一个几乎没有实际包含的 Java 代码的结构有什么可能点?

这让我们想到了使用一次性易失性读取作为建立 HB/HA 的一种方式的基本问题:没有人这样做,因此在某些时候 JVMS 更新的可能性(或者简单地将相互冲突的规则“解释”为含义:是的,热点可以消除无意义的读取,即使它确实建立了现在不再存在的 HB/HA)相当高 - 你如果您以独特的方式做事,也更有可能遇到 JVM 错误。毕竟,如果您以人们熟悉的方式做事,那么该错误早在很久以前就会被报告和修复。

如何建立HB/HA:

  1. 自然规则:在单个线程中,除了顺序之外,不能观察到代码以任何方式运行,即在一个线程中,所有行都以明显的方式彼此具有 HB/HA。

  2. 同步块:如果一个线程退出一个同步块,然后另一个线程进入一个在同一引用上,那么同步块退出发生在同步块之前-输入B。

  3. 易失性读写。

  4. 一些奇特的东西,例如:thread.start() 发生在线程的 run() 方法的第一行之前,或者线程中的所有代码都保证在该线程上的 thread.yield() 之前是 HB完成。这些往往是显而易见的。

因此,回答这个问题,它是好的编程设计吗?

,不是。

以正确的方式建立 HB/HA:在 java.util.concurrent 中找到合适的东西并使用它。从一个简单的锁到一个队列,再到整个作业的 fork/join 池。或者,停止共享状态。或者,以比 HB/HA 更自然的方式与专为并发访问而设计的机制共享状态,例如数据库(事务)或消息队列。