我试图学习Java Memory Model,但仍无法理解人们在实践中如何使用它。
我知道许多人只依赖于适当的内存障碍(如Cookbook中所述),但实际上模型本身并不运行这样的术语。 该模型引入了在一组动作上定义的不同顺序,并定义了所谓的"格式良好的执行"。 有些人试图使用其中一个命令来解释内存模型限制,即"发生在"之前,但似乎订单,至少本身并没有定义可接受的执行:
应该注意的是,两个动作之间存在的先发生关系并不一定意味着它们必须在实现中以该顺序发生。如果重新排序产生的结果与合法执行一致,则不是非法的
我的问题是如何验证某些代码或更改是否会导致非法执行"在实践中(根据模型)?
更具体地说,让我们考虑一个非常简单的例子:
public class SomeClass {
private int a;
private int b;
public void someMethod() {
a = 2; // 1
b = 3; // 2
}
// other methods
}
很明显,根据程序顺序, w(a = 2)在 w(b = 3)之前发生。 编译器/优化器如何确保重新排序1和2不会产生"非法执行" (严格按照型号而定)?为什么如果我们将 b 设置为易变的呢?
答案 0 :(得分:2)
您是否在询问 VM / JIT如何分析字节码流?这个问题太过广泛而无法回答,整篇研究论文都是关于这一点的。 VM实际实现的内容可能会因发布而异。
或者仅仅是关于记忆模型的哪些规则管理什么是合法的问题"?对于执行线程,内存模型已经强烈保证给定线程上的每个操作出现以该线程的程序顺序发生。这意味着如果JIT通过它为重新排序实现的任何方法确定重新排序产生相同的可观察结果是合法的。
对于其他线程(例如易失性访问)建立事先保证的操作的存在只会为合法重新排序添加更多约束。
简化它可以被记忆为在之前的程序顺序中发生的所有事情在执行建立操作之前似乎已经(已经)发生在其他线程上。
对于你的例子,这意味着,在非易失性(a,b)的情况下,只有保证"似乎在程序顺序中发生" (对执行线程)需要得到维护,这意味着对(a,b)的写入的任何重新排序都是合法的,甚至将它们推迟到实际读取之前(例如将值保存在CPU寄存器中并绕过主存储器)有效。如果JIT在对象超出范围之前检测到它们实际上从未被读过,它甚至可以省略写入成员(确切地说,也没有使用它们的终结器)。
在示例中使b volatile变为约束,因为其他线程读取b也可以保证看到a的最后一次更新,因为它发生在写入b之前。发生之前的操作再次简化,将执行线程中感知到的排序保证的某些扩展到其他线程。
答案 1 :(得分:0)
似乎你犯了一个常见的错误,就是过分考虑JMM的低级方面。关于你的问题“人们如何在实践中使用它”,如果你在谈论应用程序员,他会在实践中使用它,而不是一直考虑内存障碍或可能的重新排序。
关于你的例子:
public void someMethod() {
a = 2; // 1
b = 3; // 2
}
鉴于a
和b
不是final
,非volatile
。
很明显,根据程序顺序,在w(b = 3)之前,线程w(a = 2)内发生。编译器/优化器如何确保重新排序1和2不会产生"非法执行" (严格按照模型而言)?
在这里,它适用于你专注于孤立地重新排序。首先,生成的代码(HotSpot优化,JIT编译等)根本不需要将值写入堆内存。它可能在CPU寄存器中保存新值,并在同一线程的后续操作中使用它。只有在达到某一点时,必须使这些更改对其他线程可见,否则必须将它们写入堆中。这可能以任意顺序发生。
但是,例如,如果方法的调用者在调用此方法后进入无限循环,则不必编写这些值。
为什么如果我们将b设置为易变的呢?
将b
声明为volatile
确保不保证写a
和b
。这是因为专注于记忆障碍而产生的另一个错误。
让我们更加抽象:
假设您有两个并发操作,A
和B
。对于Java中的并发执行,有几种完全有效的行为,包括:
A
可能会在B
B
可能会在A
A
和B
并行运行如果B
完全在A
之前执行,那么A
中的写屏障和B
中的读屏障就没有意义{{1}仍然不会注意到B
的任何活动。您可以从这个起点得出关于不同平行场景的结论。
这是发生之前 - 关系发挥作用的地方:在{<1>}变量之前写入读取另一个线程来自该变量的值。如果在写操作之前执行读操作,则读取线程将不会看到该值,因此没有发生在之前的关系,因此没有关于我们可以进行的其他变量的声明。
保持A
为volatile
的示例:这意味着如果阅读帖子读取b
并读取值volatile
,,则只有这样保证在后续读取中b
可以看到3
的值(如果有其他写入则更新的值)。
因此,如果JVM可以证明2
上的读操作永远不会看到写入的值,可能是因为我们正在修改的整个实例永远不会被另一个线程看到,所以没有发生过 - 之前要建立的关系,换句话说,a
为b
对这种情况下允许的代码转换没有影响,即它也可能被重新排序,甚至根本没有写入堆中
所以最重要的是,查看一小段代码并询问它是否允许重新排序或是否包含内存屏障是没有用的。这甚至可能无法回答,因为答案可能会根据代码的实际使用方式而改变。只有当您的视图足够宽,才能看到线程在访问数据时如何交互,并且您可以安全地推断出是否发生 - 之前关系将建立起来,您可以开始得出关于代码正确工作的结论。正如您自己发现的那样,正确的工作并不意味着您知道重新排序是否会在最低级别发生。