简体   繁体   中英

Java Memory Model in practice

I was trying to learn the Java Memory Model , but still cannot understand how people use it in practice.

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. 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:

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. How can compiler/optimizer be sure that reordering 1 and 2 won't produce an "illegal execution" (strictly in terms of the model) ? And why if we set b to be volatile it will ?

Are you asking about how the VM/JIT analyzes the bytecode flow? 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.

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.

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. 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).

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. 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. 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 .

It's clear that within the thread w(a = 2) happens before w(b = 3) according to the program order. How can compiler/optimizer be sure that reordering 1 and 2 won't produce an "illegal execution" (strictly in terms of the model) ?

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. It might hold the new values in CPU registers and use it from there in subsequent operations of the same thread. 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 ?

Declaring b as volatile does not guaranty that a and b are written. 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 . For concurrent execution in Java, there are several perfectly valid behaviors, including:

  • A might be executed entirely before B
  • B might be executed entirely before A
  • All or parts of A and B run in parallel

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 . 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. 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.

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.


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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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