Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

The differences and relationships among Java memory model, sequential consistent memory model and original happens-before memory model are introduced in detail.

The Java memory model was designed with reference to the sequential consistent memory model and the original happens-before memory model, incorporating their strengths and improving on their weaknesses.

1 Sequential memory model

1.1 Data competition and sequence consistency guarantee

In multithreading, data contention occurs when programs are not properly synchronized. The Java Memory model specification defines data contention as: write a variable in a thread; Read the same variable in another thread; And write and read are not sorted by synchronization!

When data races are included in the code, the execution of the program often produces counterintuitive results (as was the case in the previous chapter). If a multithreaded program can synchronize correctly, it will be a program with no data contention.

The JMM guarantees memory consistency for properly synchronized multithreaded programs as follows:

If the program is correctly synchronized, the execution of the program will be sequentially consistent — that is, the execution of the program will be the same as the execution of the program in the sequentially consistent memory model.

Synchronization here refers to synchronization ina broad sense, including the proper use of common synchronization primitives (lock, volatile, and final). Let’s look at the sequential consistent memory model.

1.2 Sequential consistent memory model

The Sequential Consistency Memory Model is a theoretical reference Model idealized by computer scientists. It is a strong guarantee of visibility and sequence during program execution. At design time, the sequential consistency memory model is used as reference for both the processor’s memory model and the programming language’s memory model.

Sequential consistent memory model has two characteristics:

  1. All operations in a thread must be executed in program order.
  2. All threads (whether or not the program is synchronized) see a single order of execution. In the sequential consistent memory model, every operation must be performed atomically and immediately visible to all threads.

The sequential consistent memory model provides the programmer with the following view:

Conceptually, the sequential consistency model has a single global memory that can be connected to any thread by a switch that swings from side to side, and each thread must perform memory read/write operations in program order. At most one thread can be connected to memory at any time. When multiple threads execute concurrently, the switch serializes all memory read/write operations for all threads.

Suppose the two threads use A monitor lock to synchronize correctly: the monitor lock is released after three operations on thread A, and then thread B acquires the same monitor lock. Then the execution effect of the program in the sequential consistency model is as follows:

Now, assuming that the two threads are not synchronized, here is a schematic of the execution of the unsynchronized program in the sequential consistency model:

Although the overall execution order of the unsynchronized program is disordered in the sequential consistency model, all threads can only see A consistent overall execution order: for example, thread A and thread B see the execution order as B1->A1->A2->B2->A3->B3. This guarantee is made possible because each operation in the sequential consistent memory model must be immediately visible to any thread.

However, there is no such guarantee in the JMM. Not only is the overall execution order of an unsynchronized program out of order in the JMM, but the execution order of operations seen by all threads may be inconsistent. For example, when the current thread caches data in local memory, the write operation is visible only to the current thread until it is flushed to main memory.

From the perspective of other threads, it would appear that the write operation has not been performed by the current thread at all. Write operations are visible to other threads only after the current thread has flushed data from local memory to main memory. In this case, the actions seen by the current thread and other threads will be executed in a different order.

1.3 Effect of sequential consistency of synchronization program

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer(a) {
    a = 1;
    flag = true;
}

public synchronized void reader(a) {
    if (flag) {
        inti = a; ... }}}Copy the code

In the example code above, we assume that thread A executes writer() and thread B executes Reader (). This is a properly synchronized multithreaded program. According to the JMM specification, the execution result of this program will be the same as the execution result of the program in the sequential consistency model. The following is a comparison of the execution sequence of the program in the two memory models:

In the sequential consistency model, all operations are executed sequentially, exactly in program order. In JMM, code within a critical zone can be reordered (but the JMM does not allow code within a critical zone to “escape” outside of the critical zone, which would break the semantics of the monitor). The JMM does some special processing at the critical points of exit and entry of the monitor so that the thread has the same memory view as the sequential consistency model at those points (more on that later). Although thread A resorts within the critical region, thread B here cannot “observe” thread A’s reorder within the critical region due to the mutually exclusive nature of the monitor. This kind of reordering not only improves the execution efficiency, but also does not change the execution result of the program.

Here we can see the basic approach of the JMM implementation: leave the door open for compiler and processor optimization as much as possible without changing the results of (properly synchronized) program execution.

2.4 Execution characteristics of unsynchronized programs

For unsynchronized or improperly synchronized multithreaded programs, the JMM provides only minimal security: The value read by a thread is either a value written by a previous thread or a default value (0, null, false). The JMM guarantees that the value read by a thread will not appear out of thin air. For minimal security, when the JVM allocates objects on the heap, the memory space is cleared before objects are allocated on it (the two operations are synchronized internally within the JVM). Thus, the default initialization of the domain is done when objects are allocated in pre-zeroed memory.

The JMM does not guarantee that the execution result of an unsynchronized program will be the same as the execution result of the program in the sequential consistency model. Because the unsynchronized program executed in the sequential consistency model is disorderly on the whole, its execution result is unpredictable. It makes no sense to ensure that unsynchronized programs execute consistently in both models.

Like the sequential consistency model, the execution of unsynchronized programs in the JMM is generally disorderly and unpredictable. Meanwhile, the execution characteristics of unsynchronized programs in these two models have the following differences:

  1. The sequential consistency model guarantees that operations within a single thread will be executed in program order, whereas the JMM does not guarantee that operations within a single thread will be executed in program order (such as the reordering of a properly synchronized multithreaded program in a critical region above). This point has already been made and will not be repeated here.
  2. The sequential consistency model guarantees that all threads will see the same order of execution, whereas the JMM does not guarantee that all threads will see the same order of execution. This point has already been mentioned and will not be repeated here.
  3. The JMM does not guarantee atomicity for reads/writes to 64-bit longs and doubles, whereas the sequential consistency model guarantees atomicity for all in-memory reads/writes.

2 Original happens-before memory model

2.1 Causal relationship

The original happens-before model was too weak, and all the things that the Java memory model allowed, the happens-before model also allowed, but some things the Java memory model did not allow, such as violating causality in unpredictable ways by allowing certain values to appear out of thin air. Figure 1:

In the code above, in the original happens-before memory model for proper synchronization, there is a case where the execution result is R1 = R2 = 1. Because in the original happens-before memory model, the code might be optimized to:

This leads to the unlikely result of r1 = R2 = 1.

When a write occurs before a dependent read, the problem is called causality, because it involves the question of whether the write triggers its own occurrence. The read makes the write happen, and the write makes the read see the values they all see.

2.2 Improvements to the Java memory model

The Java memory model takes a particular execution and a program as input, and then determines whether the execution is a valid execution of that program. It does this by gradually establishing a set of “committed” actions that reflect what we know can be performed by the program without a “causal loop.”

Typically, the next action to be committed represents the next action that can be executed by a sequential execution process. However, to show that reads can see values written by other threads later in the program sequence, we allow some actions to be committed before others that occur earlier.

The Java memory model allows the behavior shown in figure 2, even though this example seems to have circular causality. This behavior must be allowed because the compiler can: eliminate redundant reads of a, replace R2 = a with R2 = R1, then make sure the expression R1 == r2 is always true, eliminate the conditional branch 3, and finally move 4: b = 2 to the front. This is very helpful for improving compiler performance.

Some actions can be submitted ahead of time, others can’t. The Java memory model does not allow the initial r1 = R2 = 1 situation in Figure 1, but allows the substitution in Figure 2. The Java memory model determines that an action will not generate data contention and allows the action to commit ahead of time.

A data race is simply defined as a write operation in one thread that is read by another thread, and the reads and writes are not ordered synchronously. When this happens, it is said that data contention exists.

For Figure 1, since the value of contention data X is not known, the Java memory model does not allow write operations to occur before reading; For Figure 2, the Jvav memory model. Although the value of a is not known, r1== R2 is always true, so optimization is allowed!

3 summary

The sequential consistent memory model is too strict for Java to be a Java memory model because it prohibits standard compiler and processor optimizations, affecting performance. Then there is the original happens-before memory model, which is already very close to the requirements of the Java memory model, but is too weak to allow the violation of causality that is unacceptable to both the Java memory model and the programmer. So Java uses a modified happens-before memory model to form its own falling-memory model, the JMM.

For details on the JMM and the improved happens-before memory model, see this article: The Java Memory Model and the Happens-before principle in detail.

References:

  1. JSR133 Specification
  2. The Art of Concurrent Programming in Java
  3. The Beauty of Concurrent Programming in Java

If you don’t understand or need to communicate, you can leave a message. In addition, I hope to like, collect, pay attention to, I will continue to update a variety of Java learning blog!