What is happens-before? The most core concept of JMM, you will understand after reading it

Happens-before is the core concept of JMM. For Java programmers, understanding happens-before is the key to understanding JMM.

In my concurrency series, the first three articles learned the three key elements of the underlying implementation of the Java concurrency mechanism: volatile , synchronized , and atomic operations . And the Java memory model is to solve the visibility and ordering problems caused by CPU cache, compiler and processor instruction reordering in a concurrent environment. Among them, we focused on learning the memory semantics of volatile, and how JMM is defined and implemented. When learning the implementation principle of volatile memory semantics, we learned that JMM's solution to instruction rearrangement actually defines a happens-before rule. Today we will come to one To get a glimpse, try to take everyone to learn happens-before in easy-to-understand words.

If you have not read the previous articles on Java memory model learning, it is recommended to go to the end of the article and click on the corresponding link to read. It is better to proceed step by step.

Next, enter the theme and start today's performance.


JMM design

To learn happens-before, first introduce the design intent of JMM. This question starts with reality:

  1. When we programmers write code, we require the memory model to be easy to understand and easy to program, so we need to rely on a strong memory model to code. That is to say, like axioms, with well-defined rules, we will be done by following the rules and writing code.
  2. For the implementation of compilers and processors, they hope to have as few constraints as possible. After all, you limit them to definitely affect their execution efficiency, and they cannot be optimized to provide performance as best they can. So they need a weak memory model.

Well, the two points mentioned above are obviously in conflict. As programmers, we hope that JMM will provide us with a strong memory model, while the underlying compiler and processor need a weak memory model to improve their performance.

In the computer field, there are many scenarios that need to be weighed, such as memory and CPU registers. CPU multi-level cache is introduced to solve performance problems, but it also introduces various problems in multi-core cpu concurrency scenarios. So here too, we need to find a balance to meet the needs of programmers, while also satisfying the compiler and processor restrictions as much as possible to maximize performance.

Therefore, JMM defines the following strategies when designing:

  1. For reordering that will change the results of program execution, JMM requires the compiler and processor to prohibit such reordering.
  2. For reordering that does not change the result of program execution, JMM does not require the compiler and processor (JMM allows this reordering).

Let's understand it in combination with this JMM design drawing:

As you can see from the figure above, JMM provides our programmers with a strong enough memory visibility guarantee. Without affecting the execution results of the program, some visibility guarantees do not exist, such as the following program, A happens-before B does not guarantee, because it does not affect the results of program execution;

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

This leads to another aspect. In order to meet the constraints of the compiler and the processor as little as possible, the JMM follows the rule: as long as the execution result of the program is not changed, the compiler and the processor can optimize as much as they want . For example, if the compiler determines that a lock can only be accessed by a single thread after careful analysis, the lock can be eliminated. For another example, if the compiler determines that a volatile variable can only be accessed by a single thread after careful analysis, the compiler can treat the volatile variable as an ordinary variable. These optimizations will not change the execution result of the program, but also improve the execution efficiency of the program.

happens-before rule

How to understand the happens-before rule? If only the literary meaning is understood as taking place first, then the opposite is true. Happens-before does not mean that the previous operation occurred before the next operation. Although there is no error from the programmer's programming point of view, it actually expresses that the result of the previous operation is visible to subsequent operations. Of .

You may ask what is the difference between these two statements?

This is because the perspective provided by JMM to programmers is executed in order, and one operation happens-before to another operation, then the execution result of the first operation will be visible to the second execution result, and the first operation The order of execution is before the second order. Note that this is a guarantee made by JMM to programmers .

But in fact, when JMM imposes constraints on the compiler and processor, as mentioned earlier, the rule is: no matter how the compiler and processor optimize without changing the execution result of the program. In other words, there is a happens-before rule between two operations. The Java platform does not necessarily execute in the order defined by the rules. The reason for this is that we programmers do not care whether the two operations are reordered, as long as the semantics cannot be changed during program execution.

The purpose of happens-before is to increase the parallelism of program execution as much as possible without changing the result of program execution.

After understanding the meaning of happens-before, let's take a look at the specific happens-before rule definition.

1. Procedure order rules

In a thread, according to the program sequence, the previous operation Happens-Before is followed by any subsequent operation . This is still very easy to understand. For example, in the three lines of code above, "double pi = 3.14;" happens-before in the first line of "double r = 1.0;". This is the content of rule 1, which is more in line with the single thread. Logical thinking, easy to understand.

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

2. Monitor lock rules

To unlock a lock, happens-before and then lock the lock.

The lock mentioned in this rule is actually synchronized in Java. For example, the following code will automatically lock before entering the synchronization block, and automatically release the lock after the code block is executed. The lock and release of the lock are implemented for us by the compiler.

synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁

Therefore, combined with the lock rule, it can be understood as: assuming that the initial value of x is 10, the value of x will become 12 after thread A executes the code block (the lock is automatically released after execution), and thread B can see the thread when it enters the code block A write operation to x, that is, thread B can see x==12. This is in line with our intuition and is very easy to understand. .

3. Volatile variable rules

Write to a volatile domain, happens-before any subsequent reading of this volatile domain

This is a bit puzzling. The write operation to a volatile variable is visible compared to the subsequent read operation to the volatile variable. This means that caching is disabled. It seems that the semantics of the version before 1.5 have not changed (previously mentioned) Before version 1.5 allows reordering between volatile variables and ordinary variables)? If you look at this rule alone, it is true, but if we associate rule 4, you can feel the change

4. Transitivity

If A happens-before B, and B happens-before C, then A happens-before C.

We apply the transitivity of Rule 4 to our example below. What will happen?

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

You can see the picture below:

From the figure, we can see:

  1. "X=42" Happens-Before writes the variable "v=true", which is the content of rule 1;
  2. Write the variable "v=true" Happens-Before Read the variable "v=true", this is the content of rule 3.
  3. According to this transitive rule, we get the result: "x=42" Happens-Before reads the variable "v=true". What does this mean?

If thread B reads "v=true", then the "x=42" set by thread A is visible to thread B. In other words, thread B can see "x == 42", is there a feeling of sudden realization? This is the enhancement of volatile semantics in version 1.5. This enhancement is of great significance. The concurrency toolkit (java.util.concurrent) of version 1.5 relies on volatile semantics to get visibility.

5. start() rules

This one is about thread startup. It means that after the main thread A starts the child thread B, the child thread B can see the operation of the main thread before the child thread B is started.

6. join() rules

If thread A performs the operation ThreadB.join() and returns successfully, then any operation in thread B happens-before that thread A successfully returns from the ThreadB.join() operation.

The combination of the happens-before rules in 6 above can provide us programmers with consistent memory visibility. Commonly used rule 1 is combined with other rules to provide us with a reliable memory visibility model for writing concurrent programs.

to sum up

In the Java language, the semantics of Happens-Before is essentially a kind of visibility. A Happens-Before B means that the A event is visible to the B event, regardless of whether the A event and the B event occur in the same thread. For example, event A occurs on thread 1, and event B occurs on thread 2. Happens-Before rule guarantees that event A can also be seen on thread 2.

The design of JMM is divided into two parts , one part is for our programmers , which is the happens-before rule. It explains a strong memory model to our programmers in an easy-to-understand manner. We only need to understand the happens-before rule. Write concurrency safe programs. The other part is implemented for the JVM . In order to impose as few constraints on the compiler and processor as possible to improve performance, JMM does not require it without affecting the program execution result, that is, it allows optimized reordering. We only need to pay attention to the former, which is to understand the happens-before rule. After all, we are programmers and we have expertise in the technical industry. It would be nice to be able to write safe concurrent programs.

 

Recommended articles:


Concurrency in Java? The three principles of the underlying implementation of the concurrency mechanism that need to be learned first

When Ali interviewed, he fell on the Java memory model

Don't you understand the volatile keyword? Hurry up and take a look

Recently I interviewed Byte and BAT, and compiled an interview material "Java Interview BAT Clearance Manual", covering Java core technologies, JVM, Java concurrency, SSM, microservices, databases, data structures, etc. How to get it: After you like it, follow the official account and reply to 666 to receive it. More content will be provided one after another

 

Guess you like

Origin blog.csdn.net/taurus_7c/article/details/105345315