In-depth understanding of the Java memory model (3)-sequential consistency

Data competition and sequence consistency guarantee

When the program is not synchronized correctly, there will be data races. The Java memory model specification defines data competition as follows:

  • Write a variable in a thread,
  • Read the same variable in another thread,
  • And write and read are not sorted by synchronization.

When the code contains data races, the execution of the program often produces counter-intuitive results (the example in the previous chapter is like this). If a multithreaded program can be synchronized correctly, the program will be a program without data competition.

JMM guarantees the memory consistency of correctly synchronized multithreaded programs as follows:

  • If the program is properly synchronized, the execution of the program will have sequential consistency (sequentially consistent) - that is, the execution result of the program is the same as the execution result of the program in the sequential consistency memory model (we will see shortly, this is for It is a strong guarantee for programmers). The synchronization here refers to synchronization in a broad sense, including the correct use of common synchronization primitives (lock, volatile, and final).

 

Sequential consistent memory model

The sequential consistency memory model is a theoretical reference model idealized by computer scientists. It provides programmers with a strong guarantee of memory visibility. The sequential consistency memory model has two major characteristics :

  • All operations in a thread must be executed in the order of the program.
  • (Regardless of whether the program is synchronized or not) All threads can only see a single order of execution of operations. In the sequential consistent memory model, each operation must be executed atomically and visible to all threads at once.

The sequential consistent memory model provides programmers with the following views:

Conceptually, the sequential consistency model has a single global memory, which can be connected to any thread through a left and right switch. At the same time, each thread must perform memory read/write operations in the order of the program. From the above figure, we can see that at most one thread can be connected to the memory at any point in time. When multiple threads execute concurrently, the switch device in the figure can serialize all memory read/write operations of all threads.

For a better understanding, let's use two schematic diagrams to further explain the characteristics of the sequential consistency model.

Suppose there are two threads A and B executing concurrently. The A thread has three operations, and their order in the program is: A1->A2->A3. B thread also has three operations, their order in the program is: B1->B2->B3.

Assume that these two threads use monitors to properly synchronize: A thread releases the monitor after the three operations are performed, and then B thread acquires the same monitor. Then the execution effect of the program in the sequential consistency model will be as shown in the following figure:

Now let us assume that the two threads are not synchronized. The following is a schematic diagram of the execution of this unsynchronized program in the sequential consistency model:

Although the overall execution order of unsynchronized programs in the sequential consistency model is disordered, all threads can only see a consistent overall execution order. Take the above figure as an example, the execution order seen by threads A and B is: B1->A1->A2->B2->A3->B3. This guarantee is obtained because each operation in the sequential consistent memory model must be immediately visible to any thread.

However, there is no such guarantee in JMM. Not only the overall execution order of unsynchronized programs in JMM is disordered, but the execution order of operations seen by all threads may also be inconsistent. For example, before the current thread caches the written data in the local memory and has not flushed it to the main memory, the write operation is only visible to the current thread; from the perspective of other threads, it will be considered that the write operation has not yet occurred. Executed by the current thread. Only after the current thread flushes the data written in the local memory to the main memory, can this write operation be visible to other threads. In this case, the execution order of operations seen by the current thread and other threads will be inconsistent.

 

The sequential consistency effect of the synchronization program

Let's use the monitor to synchronize the previous example program ReorderExample to see how the properly synchronized program has sequential consistency.

Please see the sample code below:

class SynchronizedExample {

    int a = 0;
    boolean flag = false;

    public synchronized void writer() {

        a = 1;
        flag = true;
    }

    public synchronized void reader() {

        if (flag) {
            int i = a;
            ……
        }
    }

}

In the above sample code, suppose that after thread A executes the writer() method, thread B executes the reader() method. This is a properly synchronized multithreaded program. According to the JMM specification, the execution result of the program will be the same as the execution result of the program in the sequential consistency model. The following is a comparison diagram of the execution sequence of the program in the two memory models:

In the sequential consistency model, all operations are executed serially in the order of the program. In JMM, the code in the critical section can be reordered (but JMM does not allow the code in the critical section to "escape" outside the critical section, which would destroy the semantics of the monitor). JMM will do some special processing at the two key time points of exiting the monitor and entering the monitor, so that the thread has the same memory view as the sequential consistency model at these two time points (the details will be explained later). Although thread A has reordered in the critical area, due to the exclusive execution characteristics of the monitor, thread B here cannot "observe" the reordering of thread A in the critical area at all. This kind of reordering not only improves the execution efficiency, but also does not change the execution result of the program.

From here, we can see the basic policy of JMM in specific implementation: as far as possible to open the door to the optimization of the compiler and the processor without changing the (correctly synchronized) program execution results.

 

Execution characteristics of unsynchronized programs

For unsynchronized or incorrectly synchronized multi-threaded programs, JMM only provides minimal safety: the value read during the execution of the thread is either the value written by a previous thread or the default value (0, null, false) , JMM guarantees that the value read by the thread read operation will not emerge out of thin air. In order to achieve minimum security, when JVM allocates objects on the heap, it first clears the memory space, and then allocates objects on it (the JVM will synchronize these two operations internally). Therefore, when the object is allocated with pre-zeroed memory, the default initialization of the domain has already been completed.

JMM does not guarantee that the execution result of an unsynchronized program is consistent with the execution result of the program in the sequential consistency model. Because when the unsynchronized program is executed in the sequential consistency model, it is out of order as a whole, and its execution result cannot be predicted. It is meaningless to ensure that the execution results of unsynchronized programs in the two models are consistent.

Like the sequential consistency model, when an unsynchronized program is executed in JMM, it is also out of order as a whole, and its execution result is unpredictable. At the same time, the execution characteristics of unsynchronized programs in these two models have the following differences:

  1. The sequential consistency model guarantees that operations in a single thread will be executed in the order of the program, while JMM does not guarantee that operations in a single thread will be executed in the order of the program (such as the reordering of the correctly synchronized multi-threaded program above in the critical section). This point has been mentioned before, so I won't repeat it here.
  2. The sequential consistency model guarantees that all threads can only see the same order of execution of operations, while JMM does not guarantee that all threads can see the same order of execution of operations. This point has already been discussed before, so I won't repeat it here.
  3. JMM does not guarantee the atomicity of read/write operations on 64-bit long and double variables, while the sequential consistency model guarantees atomicity for all memory read/write operations.

The third difference is closely related to the working mechanism of the processor bus. In a computer, data is transferred between the processor and the memory through the bus. Each data transfer between the processor and the memory is completed through a series of steps, this series of steps is called a bus transaction (bus transaction). Bus transactions include read transactions and write transactions. A read transaction transfers data from the memory to the processor, and a write transaction transfers data from the processor to the memory. Each transaction reads/writes one or more physically consecutive words in the memory. The key here is that the bus will synchronize transactions that attempt to use the bus concurrently. During the execution of a bus transaction by a processor, the bus prohibits all other processors and I/O devices from performing memory read/write. Let us illustrate the working mechanism of the bus through a schematic diagram:

As shown in the figure above, assuming that processors A, B and C initiate bus transactions to the bus at the same time, then bus arbitration will rule the competition. Here we assume that after the bus arbitration, processor A is determined to win the competition ( Bus arbitration will ensure that all processors can access memory fairly). At this time, processor A continues its bus transaction, and the other two processors have to wait for processor A's bus transaction to complete before starting to perform memory access again. Suppose that during the execution of a bus transaction by processor A (regardless of whether the bus transaction is a read transaction or a write transaction), processor D initiates a bus transaction to the bus, and this request of processor D will be inhibited by the bus at this time.

These working mechanisms of the bus can serialize the access of all processors to the memory; at any point in time, only one processor can access the memory. This feature ensures that the memory read/write operations in a single bus transaction are atomic.

On some 32-bit processors, if the read/write operations of 64-bit data are required to be atomic, there will be a relatively large overhead. In order to take care of this processor, the Java language specification encourages but does not force the JVM to read/write 64-bit long variables and double variables atomically. When JVM runs on this kind of processor, it will split the read/write operation of a 64-bit long/double variable into two 32-bit read/write operations for execution. These two 32-bit read/write operations may be allocated to different bus transactions for execution. At this time, the read/write of this 64-bit variable will not be atomic.

When a single memory operation is not atomic, it may have unintended consequences. Please see the diagram below:

As shown in the figure above, suppose that processor A writes a long variable, and processor B wants to read this long variable at the same time. The 64-bit write operation in processor A is split into two 32-bit write operations, and the two 32-bit write operations are allocated to different write transactions for execution. At the same time, the 64-bit read operation in processor B is split into two 32-bit read operations, and the two 32-bit read operations are allocated to the same read transaction for execution. When processors A and B are executed according to the timing shown in the figure above, processor B will see invalid values ​​that are only "written in half" by processor A.

Previous articleIn-     depth understanding of the Java memory model (2)-reordering
Next article In-     depth understanding of Java memory model (4)-volatile keyword

Thanks to the author for his contribution to this article

Cheng Xiaoming, Java software engineer, nationally certified system analyst and information project manager. Focus on concurrent programming and work at Fujitsu Nanda. Personal email: [email protected].
---------------------
Author: World coding
Source: CSDN
Original: https://blog.csdn.net/dgxin_605/article/details/86183376
Copyright: This article is the original article of the blogger, please attach a link to the blog post if you reprint it!

Guess you like

Origin blog.csdn.net/dgxin_605/article/details/86183376