5
头图

Preface

In concurrent programming, when multiple threads access the same shared variable at the same time, it will produce uncertain results, so it is necessary to write thread-safe code, which is essentially access to these variable shared variables Operations are managed. The reason for this uncertain result is visibility, ordering and atomicity problems, Java to solve the visibility and ordering problem introduced Java memory model , using mutual exclusion solution (its core The technology is lock) to solve the atomicity problem. This article first takes a look at the Java Memory Model (JMM) that solves the problem of visibility and order.

What is the Java memory model

The Java memory model Wikipedia as follows:

The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language.

The memory model limits shared variables, that is, variables stored in the heap memory. In the Java language, all instance variables, static variables, and array elements are stored in the heap memory. Local variables such as method parameters and exception handling parameters are stored in the method stack frame, so they will not be shared between threads, will not be affected by the memory model, and there will be no memory visibility issues.

Generally, there are two ways of communication between threads: shared memory and message passing. Obviously, Java uses the first shared memory model . In the shared memory model, programs are shared between multiple threads. The public state, implicitly communicates through read-write memory.

From an abstract point of view, JMM is actually defines the relationship between threads and main memory. First of all, shared variables between multiple threads are stored in main memory, and each thread has its own private local Memory, the local memory stores a copy of the shared variable that the thread reads or writes (note: local memory is an abstract concept defined by JMM and does not actually exist). The abstract model is shown in the following figure:

1.png

In this abstract memory model, when communicating between two threads (shared variable state changes), the following two steps are performed:

  1. Thread A flushes the value of the shared variable copy after updating the local memory to the main memory.
  2. When thread B uses the shared variable, it reads the updated shared variable value of thread A in the main memory, and updates the value of thread B's local memory.

JMM is essentially an abstraction on top of the hardware (processor) memory model, so that application developers only need to understand JMM to write correct concurrent code without having to understand the hardware-level memory model too much.

Why the Java memory model is needed

In daily program development, the scene of assigning values to some shared variables will often be encountered. Assuming that a thread assigns an count count = 9527; ), there will be a problem at this time. Others reading the shared variable Under what circumstances does the thread get the variable value 9527 ? If there is a lack of synchronization, there will be many factors that cause other threads that read the variable to not be able to see the latest value of the variable immediately or even forever.

For example, the cache may change the order in which copies of shared variables are written to the main memory. The values stored in the local cache are invisible to other threads; the compiler sometimes changes the order of execution of statements in the program to optimize performance. , These factors may cause other threads to be unable to see the latest value of the shared variable.

At the beginning of the article, I mentioned that JMM mainly to solve the visibility and order issues, so we must first figure out what is the essential reason for the visibility and order issues? Most of the current services are running on multi-core CPU servers, each CPU has its own cache, then the CPU cache and memory data will have a consistency problem, when a thread modifies shared variables, The other thread cannot be seen immediately. The essential cause of the visibility problem is cache .

2.png

as-if-serial means that the actual execution order of the code is consistent with the order defined by the code. In order to optimize performance, the compiler will comply with the semantics of 06125c052b8439 (no matter how reordered, the execution result in a single thread cannot be changed), but sometimes the compiler And the optimization of the interpreter may also cause some problems. For example: double check to create a single instance object. Here is the code that uses double checking to delay the creation of a singleton object:

/**
 * @author mghio
 * @since 2021-08-22
 */
public class DoubleCheckedInstance {

  private static DoubleCheckedInstance instance;

  public static DoubleCheckedInstance getInstance() {
    if (instance == null) {
      synchronized (DoubleCheckedInstance.class) {
        if (instance == null) {
          instance = new DoubleCheckedInstance();
        }
      }
    }

    return instance;
  }
  
}

The instance = new DoubleCheckedInstance(); here seems to Java only one line of code for 06125c052b8470, which should not be able to be reordered. In fact, the actual instructions after compilation are as follows:

  1. Allocate the memory space of the object
  2. Initialize the object
  3. Set instance to point to the memory address that has just been allocated

The above steps 2 and 3 will not change the execution result of the single thread if the execution order is changed, that is to say, reordering may occur. The following figure is a scenario of multi-threaded concurrent execution:

3.png

At this time, the instance obtained by thread B has not been initialized. If instance a null pointer exception may be triggered. ordering problem is compiler optimization. Then you might be thinking that since caching and compiler optimization are the causes of visibility problems and ordering problems, can't these problems be completely solved by simply disabling them, but if you do, the performance of the program may be It will be greatly affected.

In fact, you can change your way of thinking. Can you give the coding engineers the right to disable caching and compiler optimizations? They must know best when they need to be disabled, so they only need to provide on-demand methods to disable caching and compiler optimizations. That is, it is more flexible to use. Therefore, the Java memory model was born. It specifies how the JVM provides on-demand disabling of caching and compilation optimization. It specifies that the JVM must comply with a set of minimum guarantees. This minimum guarantee specifies how threads write operations to shared variables. When visible to other threads.

Sequential consistent memory model

The sequential consistency model is an idealized theoretical reference model, and the design of the memory model of the processor and programming language is the sequential consistency model theory. It has the following two major characteristics:

  1. All operations in a thread must be executed in the order of the program
  2. All threads can only see a single execution sequence, regardless of whether the program is synchronized or not

The sequential consistency model from the engineer's perspective is as follows:

4.png

The sequential consistency model has a single global memory. This global memory can be connected to any thread through a swing switch. Each thread must perform memory read and write operations in the order of the program. Under this ideal model, only one thread can be connected to the memory at a task at any time. When multiple threads are executed concurrently, the read and write operations of multiple threads can be serialized by switching.

In the sequential consistency model, all operations are executed completely in sequence, but there is no guarantee in JMM. unsynchronized programs are not only out of order in JMM, but also due to the existence of local memory , The order of operations seen by all threads may also be inconsistent. For example, a thread saves write shared variables in local memory. Before flushing to main memory, other threads are invisible. Only after updating to main memory, other threads are invisible. It is possible to see the thread.

JMM guarantees the sequence consistency of the program that is correctly synchronized , that is, the execution result of the program is the same as the execution result of the program in the sequential consistency memory model.

Happens-Before rules

Happens-Before rule is the core concept in JMM. The Happens-Before concept was first paper , which uses Happens-Before to define the partial ordering relationship between distributed systems. Use Happens-Before in JSR-133 to specify the order of execution between two operations.

JMM It is through this rule to ensure visibility across threads of memory, Happens-Before meaning is a follow-up operation on the shared variable operating results are visible in front of the variables, constraints optimized compiler's behavior, while allowing the compiler Optimized, but the optimized code must meet the Happens-Before rule. This rule gives engineers this guarantee: Synchronous multi-threaded programs are executed in the order specified by Happens-Before The purpose of to improve the efficiency of program execution as much as possible without changing the execution result of the program (single-threaded or correctly synchronized multi-threaded program).

5.png

JSR-133 specification defines the following 6 Happens-Before rules:

  1. program sequence rules: each operation in a thread, Happens-Before any subsequent operations in the thread
  2. monitor lock rules: a lock, Happens-Before locks the lock later
  3. volatile rule write operation to a variable of type volatile Happens-Before and any subsequent read operation volatile
  4. transitive rule: If operation A Happens-Before in operation B, and operation B Happens-Before in operation C, then operation A Happens-Before in operation C
  5. start() rule: If a thread A performs operation threadB.start() start thread B, then thread A's start() operation Happens-Before any operation on thread B
  6. join() rule: If thread A performs operation threadB.join() and returns successfully, then any operation Happens-Before in thread B returns successfully threadB.join()

A basic principle of JMM is: As long as the execution results of single-threaded and correctly synchronized multi-threaded are not changed, the compiler and processor can be optimized at will. In fact, for application developers, whether the two operations are really reordered and combined Don't care, what really cares about is that the execution result cannot be modified. Thus Happens-Before nature and sa-if-serial semantics is the same, but sa-if-serial only to ensure that the single thread execution result is not changed.

Summarize

This article mainly introduces the basic knowledge and related concepts of the memory model. JMM shields the differences between the memory models of different processors and abstracts a unified Java memory model (JMM) for application developers on different processor platforms. . The common processor memory model is weaker than that of JMM, so JVM will insert memory barriers in appropriate locations when generating bytecode instructions (the type of memory barriers will vary depending on the processor platform) to limit partial reordering.


mghio
446 声望870 粉丝