问题描述
看看这里的答案 (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 的概念:发生在之前/发生在之后。这是它的工作原理:
- 有一个特定交互的列表,其中设置了某些代码行被定义为“发生在之前”(HB/HA = 发生在之前/发生在之后)另一行。下面给出了这个列表的概念。
- 对于具有 HBHA 关系的任何两条线,HA 线不可能观察到任何状态,以至于 HB 线似乎尚未运行。它基本上是在说:HB 线出现在 HA 线之前,除非不是那么强:你不能观察到相反的情况(即 HB 改变变量 X,HA 线没有看到这种变化到 X,那会观察到相反的,这是不可能的)。除了时间方面。实际上,HB/HA 并不意味着行会更早或更晚执行:如果您有 2 行具有 HB/HA 关系且互不影响的行(一个写入变量 X,另一个读取完全不同的变量 Y) ,一起工作的 JVM/CPU 可以随意重新排序。
- 对于没有定义 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:
-
自然规则:在单个线程中,除了顺序之外,不能观察到代码以任何方式运行,即在一个线程中,所有行都以明显的方式彼此具有 HB/HA。
-
同步块:如果一个线程退出一个同步块,然后另一个线程进入一个在同一引用上,那么同步块退出发生在同步块之前-输入B。
-
易失性读写。
-
一些奇特的东西,例如:
thread.start()
发生在线程的 run() 方法的第一行之前,或者线程中的所有代码都保证在该线程上的thread.yield()
之前是 HB完成。这些往往是显而易见的。
因此,回答这个问题,它是好的编程设计吗?
不,不是。
以正确的方式建立 HB/HA:在 java.util.concurrent
中找到合适的东西并使用它。从一个简单的锁到一个队列,再到整个作业的 fork/join 池。或者,停止共享状态。或者,以比 HB/HA 更自然的方式与专为并发访问而设计的机制共享状态,例如数据库(事务)或消息队列。