简体   繁体   English

实践中的 Java 内存模型

[英]Java Memory Model in practice

I was trying to learn the Java Memory Model , but still cannot understand how people use it in practice.我试图学习Java Memory Model ,但仍然无法理解人们如何在实践中使用它。

I know that many just rely on appropriate memory barriers (as described in the Cookbook ), but in fact the model itself does not operate such terms.我知道很多只是依赖适当的内存屏障(如Cookbook中所述),但实际上模型本身并不操作这些术语。 The model introduces different orders defined on a set of actions and defines so called "well-formed executions".该模型引入了定义在一组动作上的不同顺序,并定义了所谓的“格式良好的执行”。 Some people are trying to explain the memory model restrictions using one of such orders, namely "happens-before", but it seems like the order, at least by itself, does not define acceptable execution:有些人试图使用这样的命令之一(即“happens-before”)来解释内存模型限制,但似乎该命令至少本身并没有定义可接受的执行:

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation.应该注意的是,两个动作之间存在先发生的关系并不一定意味着它们必须在实现中按该顺序发生。 If the reordering produces results consistent with a legal execution, it is not illegal如果重新排序产生与合法执行一致的结果,则不违法

My question is how can one verify that certain code or change can lead to an "illegal execution" in practice (according to the model) ?我的问题是如何验证某些代码或更改在实践中会导致“非法执行”(根据模型)?

To be more concrete, let's consider a very simple example:更具体地说,让我们考虑一个非常简单的例子:

public class SomeClass {
   private int a;
   private int b;

   public void someMethod() {
      a = 2; // 1 
      b = 3; // 2  
   }   
   // other methods
}

It's clear that within the thread w(a = 2) happens before w(b = 3) according to the program order.很明显,根据程序顺序,在线程内w(a = 2)发生在w(b = 3)之前。 How can compiler/optimizer be sure that reordering 1 and 2 won't produce an "illegal execution" (strictly in terms of the model) ?编译器/优化器如何确保重新排序 1 和 2 不会产生“非法执行”(严格按照模型)? And why if we set b to be volatile it will ?为什么如果我们将b设置为 volatile 呢?

Are you asking about how the VM/JIT analyzes the bytecode flow?您是在问 VM/JIT如何分析字节码流吗? Thats far too broad to answer, entire research papers have been written about that.这太宽泛了,无法回答,已经写了整篇研究论文。 And what the VM actually implements may change from release to release.并且 VM 实际实现的内容可能会因发行版而异。

Or is the question simply about which rules of the memory model govern what is "legal"?或者问题仅仅是关于记忆模型的哪些规则决定什么是“合法的”? For the executing thread, the memory model already makes the strong guarantee that every action on a given thread appears to happen in program order for that thread.对于正在执行的线程,内存模型已经强有力地保证给定线程上的每个操作似乎都按照该线程的程序顺序发生。 That means if the JIT determines by whatever method(s) it implements for reordering that the reordering produces the same observable result(s) is legal.这意味着如果 JIT 通过它实现的任何重新排序方法确定重新排序产生相同的可观察结果是合法的。

The presence of actions that establish happens-before guarantees with respect to other threads (such as volatile accesses) simply adds more constraints to the legal reorderings.相对于其他线程(例如易失性访问)建立先发生前保证的操作的存在只会为合法重新排序添加更多约束。

Simplified it could be memorized as that everything that happened in program order before also appears to have (already) happened to other threads when a happend-before establishing action is executed.简化它可以被记住,因为在执行之前发生的建立操作时,之前按程序顺序发生的所有事情似乎也(已经)发生在其他线程上。

For your example that means, in case of non-volatile (a, b) only the guarantee "appears to happen in program order" (to the executing thread) needs to be upheld, that means any reordering of the writes to (a, b) is legal, even delaying them until they are actually read (eg holding the value in a CPU register and bypassing main memory) would be valid.对于您的示例,这意味着,在非易失性 (a, b) 的情况下,仅需要支持“似乎按程序顺序发生”(对执行线程)的保证,这意味着对 (a, b) 是合法的,即使延迟它们直到它们被实际读取(例如将值保存在 CPU 寄存器中并绕过主存储器)也是有效的。 It could even omit writting the members at all if the JIT detects they are never actually read before the object goes out of scope (and to be precise, there is also no finalizer using them).如果 JIT 检测到在对象超出范围之前从未真正读取过成员(并且准确地说,也没有使用它们的终结器),它甚至可以完全省略写入成员。

Making b volatile in your example changes the constraints in that other threads reading b would also be guaranteed to see the last update of a because it happened before the write to b.在您的示例中使 b 可变会更改约束,因为其他线程读取 b 也可以保证看到 a 的最后更新,因为它发生在写入 b 之前。 Again simplified, happens-before actions extend some of the perceived ordering guarantees from the executing thread to other threads.再次简化,发生在动作之前将一些感知的排序保证从执行线程扩展到其他线程。

It seems you are making the common mistake of thinking too much about low level aspects of the JMM in isolation.您似乎犯了一个常见的错误,即孤立地考虑 JMM 的低级方面。 Regarding your question “how people use it in practice”, if you are talking about an application programmer, (s)he will use it in practice by not thinking about memory barriers or possible reorderings all the time.关于你的问题“人们如何在实践中使用它”,如果你在谈论一个应用程序程序员,他会在实践中使用它,而不是一直考虑内存障碍或可能的重新排序。

Regarding your example:关于你的例子:

public void someMethod() {
  a = 2; // 1 
  b = 3; // 2  
}   

Given a and b are non- final , non- volatile .给定ab是非final ,非易失volatile

It's clear that within the thread w(a = 2) happens before w(b = 3) according to the program order.很明显,根据程序顺序,在线程内 w(a = 2) 发生在 w(b = 3) 之前。 How can compiler/optimizer be sure that reordering 1 and 2 won't produce an "illegal execution" (strictly in terms of the model) ?编译器/优化器如何确保重新排序 1 和 2 不会产生“非法执行”(严格按照模型)?

Here, it backfires that you are focusing on re-ordering in isolation.在这里,您专注于孤立地重新排序会适得其反。 First of all, the resulting code (of HotSpot optimization, JIT compilation, etc.) does not need to write the values to the heap memory at all.首先,生成的代码(HotSpot 优化、JIT 编译等)根本不需要将值写入堆内存。 It might hold the new values in CPU registers and use it from there in subsequent operations of the same thread.它可能将新值保存在 CPU 寄存器中,并在同一线程的后续操作中使用它。 Only when reaching a point were these changes have to be made visible to other threads they have to be written to the heap.只有当到达某个点时,这些更改才必须对其他线程可见,它们必须写入堆。 Which may happen in arbitrary order.这可能以任意顺序发生。

But if, for example, the caller of the method enters an infinite loop after calling this method, the values don't have to be written ever.但是,例如,如果方法的调用者在调用此方法后进入无限循环,则不必写入值。

And why if we set b to be volatile it will ?为什么如果我们将 b 设置为 volatile 呢?

Declaring b as volatile does not guaranty that a and b are written.b声明为volatile不能保证写入ab This is another mistake which arises from focusing on memory barriers.这是另一个由关注内存障碍引起的错误。

Let's go more abstract:让我们更抽象一点:

Suppose you have two concurrent actions, A and B .假设您有两个并发操作AB For concurrent execution in Java, there are several perfectly valid behaviors, including:对于 Java 中的并发执行,有几种完全有效的行为,包括:

  • A might be executed entirely before B A可能在B之前完全执行
  • B might be executed entirely before A B可能在A之前完全执行
  • All or parts of A and B run in parallel AB全部或部分并行运行

in the case B is executed entirely before A , there is no sense in having a write barrier in A and a read barrier in B , B will still not notice any activities of A .在壳体B完全之前执行A ,存在具有在写屏障没有意义A ,并在读屏障BB将仍然不会注意到的任何活动A You can draw your conclusions about different parallel scenarios from this starting point.您可以从这个起点得出关于不同并行场景的结论。

This is where the happens-before relationship comes into play: a write of a value to a volatile variable happens before a read of that value from that variable by another thread.这就是happens-before关系发挥作用的地方:在另一个线程从该变量读取该值之前,将一个值写入一个volatile变量。 If the read operation is executed before the write operation, the reading thread will not see the value and hence there's no happens-before relationship and so there is no statement about the other variables we can make.如果读取操作在写入操作之前执行,读取线程将看不到该值,因此没有发生之前的关系,因此没有关于我们可以创建的其他变量的声明。

To stay at your example with b being volatile : this implies that if a reading thread reads b and reads the value 3 , and only then it is guaranteed to see the value of 2 (or an even more recent value if there are other writes) for a on subsequent reads.留在你的例子bvolatile :这意味着如果一个读取线程读取b并读取值3只有这样才能保证看到2的值(或者如果有其他写入则是更新的值)对于a在后续读取。

So if a JVM can prove that there will never be a read operation on b seeing the written value, maybe because the entire instance we are modifying will never be seen by another thread, there is no happens-before relationship to be ever established, in other words, b being volatile has no impact on the allowed code transformations in this case, ie it might be reordered as well, or even never written to the heap at all.因此,如果 JVM 可以证明永远不会对b进行读操作看到写入的值,也许是因为我们正在修改的整个实例永远不会被另一个线程看到,那么就不会建立任何发生之前的关系,在换句话说,在这种情况下, bvolatile对允许的代码转换没有影响,即它也可能被重新排序,甚至根本不会写入堆。


So the bottom line is that it is not useful to look at a small piece of code and ask whether it will allow reordering or whether it will contain a memory barrier.所以最重要的是,查看一小段代码并询问它是否允许重新排序或是否包含内存屏障是没有用的。 This might not even be answerable as the answer might change depending on how the code is actually used.这甚至可能无法回答,因为答案可能会根据代码的实际使用方式而改变。 Only if your view is wide enough to see how threads will interact when accessing the data and you can safely deduce whether a happens-before relationship will be established you can start drawing conclusions about the correct working of the code.只有当您的视图足够宽以查看线程在访问数据时将如何交互,并且您可以安全地推断是否会建立发生之前的关系时,您才能开始得出有关代码正确工作的结论。 As you found out by yourself, correct working does not imply that you know whether reordering will happen or not on the lowest level.正如您自己发现的那样,正确的工作并不意味着您知道重新排序是否会在最低级别发生。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM