Comprehensive understanding of the Java memory model

Reprinted from: https://blog.csdn.net/suifeng3051/article/details/52611310

The Java Memory Model is the Java Memory Model, or JMM for short. JMM defines how the Java Virtual Machine (JVM) works in computer memory (RAM). The JVM is a virtual model of the entire computer, so the JMM belongs to the JVM.

If we want to deeply understand Java concurrent programming, we must first understand the Java memory model. The Java memory model defines the visibility of shared variables between multiple threads and how to synchronize shared variables when needed. The efficiency of the original Java memory model is not very ideal, so the Java 1.5 version has been refactored, and the current Java 8 still uses the Java 1.5 version.

About concurrent programming

In the field of concurrent programming, there are two key issues: communication and synchronization between threads .

communication between threads

Thread communication refers to the mechanism by which threads exchange information. In imperative programming, there are two mechanisms for communication between threads, shared memory and message passing .

In the shared memory concurrency model, the common state of the program is shared between threads, and the threads communicate implicitly by writing-reading the common state in the memory. A typical shared memory communication method is to communicate through shared objects .

In the concurrency model of message passing , there is no public state between threads, and threads must communicate explicitly by sending messages explicitly. The typical message passing methods in Java are wait() and notify() .

For the communication between Java threads, you can refer to the communication between threads (thread signal) .

synchronization between threads

Synchronization refers to the mechanism that a program uses to control the relative order in which operations occur between different threads.

In the shared memory concurrency model, synchronization is done explicitly. The programmer must explicitly specify that a method or a section of code needs to execute mutually exclusive among threads.

In the concurrency model of message passing, synchronization is implicit because the sending of messages must precede the receipt of messages.

Java's concurrency uses a shared memory model

Communication between Java threads is always implicit, and the entire communication process is completely transparent to the programmer. If a Java programmer writing a multithreaded program doesn't understand how the implicit inter-thread communication works, it's likely to run into all sorts of weird memory visibility issues.

Java memory model

As mentioned above, the communication between Java threads adopts the shared memory model. The shared memory model mentioned here refers to the Java Memory Model (JMM for short). JMM determines when a thread writes a shared variable to another. Thread is visible . From an abstract point of view, JMM defines an abstract relationship between threads and main memory: shared variables between threads are stored in main memory (main memory), and each thread has a private local memory (local memory) , a copy of the thread to read/write shared variables is stored in local memory . Local memory is an abstraction of JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations.

write picture description here

From the above figure, if thread A and thread B want to communicate, they must go through the following two steps:

1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 
  • 1
  • 2
  • 3

The two steps are illustrated in the following diagram: 
write picture description here

As shown in the figure above, local memory A and B have copies of the shared variable x in main memory. Assume that initially, the x values ​​in these three memories are all 0. When thread A is executing, it temporarily stores the updated x value (assuming the value is 1) in its own local memory A. When thread A and thread B need to communicate, thread A first refreshes the modified x value in its local memory to the main memory, and the x value in the main memory becomes 1 at this time. Then, thread B goes to the main memory to read the updated x value of thread A, and the x value of thread B's local memory also becomes 1.

On the whole, these two steps are essentially thread A sending a message to thread B, and this communication process must go through the main memory. JMM provides memory visibility guarantees for java programmers by controlling the interaction between main memory and each thread's local memory.

As mentioned above, the Java memory model is just an abstract concept, so how does it work in Java? In order to better understand how the Java memory model works, the following will give a detailed introduction to the implementation of the Java memory model, the hardware memory model and the bridge between them by the JVM.

Implementation of Java Memory Model by JVM

Inside the JVM, the Java memory model divides memory into two parts: the thread stack area and the heap area. The following figure shows the logical view of the Java memory model in the JVM: 
write picture description here 
Each thread running in the JVM has its own thread stack. The stack contains information about method calls executed by the current thread, and we also call it the call stack. As the code continues to execute, the call stack will continue to change.

The thread stack also contains all the local variable information of the current method. A thread can only read its own thread stack, that is, the local variables in the thread are not visible to other threads. Even if two threads are executing the same piece of code, they each create local variables in their own thread stacks, so each thread will have its own version of local variables.

All local variables of primitive types (boolean, byte, short, char, int, long, float, double) are directly stored in the thread stack, and their values ​​are independent between threads. For local variables of primitive types, one thread can pass a copy to another thread when they cannot be shared between them.

The heap area contains all the object information created by the Java application, no matter which thread the object is created by, and the objects include the encapsulation classes of primitive types (such as Byte, Integer, Long, etc.). Whether the object belongs to a member variable or a local variable in a method, it is stored on the heap.

The figure below shows that the call stack and local variables are stored in the stack area, and the objects are stored in the heap area: 
write picture description here 
a local variable of a primitive type is completely stored in the stack area. 
A local variable may also be a reference to an object, in which case the local reference is stored on the stack, but the object itself is still stored on the heap.

For member methods of an object, these methods contain local variables and still need to be stored in the stack area, even if the object they belong to is in the heap area. 
For a member variable of an object, whether it is a primitive type or a wrapper type, it will be stored in the heap area.

Static type variables and information about the class itself will be stored in the heap area along with the class itself.

Objects in the heap can be shared by multiple threads. If a thread obtains an application of an object, it can access the object's member variables. If two threads call the same method of the same object at the same time, then the two threads can access the member variables of the object at the same time, but for local variables, each thread will copy a copy to its own thread stack.

The following diagram illustrates the process described above: 
write picture description here

hardware memory architecture

No matter what the memory model is, it will eventually run on computer hardware, so it is necessary to understand the computer hardware memory architecture. The following figure briefly describes the contemporary computer hardware memory architecture: 
write picture description here

Modern computers generally have more than 2 CPUs, and each CPU may contain multiple cores. Therefore, if our application is multi-threaded, these threads may run in parallel on each CPU core.

There is a set of CPU registers inside the CPU, that is, the memory of the CPU. The CPU can manipulate registers much faster than the computer's main memory. There is also a CPU cache between the main memory and the CPU registers. The CPU operates the CPU cache faster than the main memory but slower than the CPU registers. Some CPUs may have multiple cache layers (L1 and L2). The main memory of a computer is also called RAM. All CPUs can access the main memory, and the main memory is much larger than the caches and registers mentioned above.

When a CPU needs to access the main memory, it will first read a part of the main memory data to the CPU cache, and then read the CPU cache to the register. When the CPU needs to write data to the main memory, it will also flush the registers to the CPU cache first, and then flush the cached data to the main memory at some nodes.

Bridge between Java memory model and hardware architecture

As mentioned above, the Java memory model is not consistent with the hardware memory architecture. There is no distinction between stack and heap in the hardware memory architecture. From a hardware perspective, most of the data will be stored in the main memory, whether it is the stack or the heap. Of course, some of the stack and heap data may also be stored in the CPU registers, as shown below. As shown, the Java memory model and the computer hardware memory architecture are a cross relationship: 
write picture description here 
when objects and variables are stored in various memory areas of the computer, there are bound to be some problems. The two main problems are:

1. 共享对象对各个线程的可见性
2. 共享对象的竞争现象
  • 1
  • 2
  • 3

Shared Object Visibility

When multiple threads operate on the same shared object at the same time, if the volatile and synchronization keywords are not used reasonably, the update of one thread to the shared object may cause other threads to be invisible.

Imagine that our shared object is stored in main memory, a thread in the CPU reads the main memory data to the CPU cache, and then makes changes to the shared object, but the changed object in the CPU cache has not been flushed to the main memory, this Changes to a shared object by a thread are not visible to threads in other CPUs. In the end, each thread will eventually copy the shared object, and the copied object is located in a different CPU cache.

The following diagram illustrates the process described above. The thread running on the left CPU copies the shared object obj from main memory to its CPU cache and changes the count variable of the object obj to 2. But this change is not visible to the thread running on the right CPU, because the change has not been flushed to main memory: 
write picture description here 
to solve this problem of shared object visibility, we can use the java volatile keyword. Java's volatile keyword. The volatile keyword ensures that variables are read directly from main memory, and updates to variables are also written directly to main memory. The volatile principle is implemented based on the CPU memory barrier instruction, which will be discussed later.

competition phenomenon

If multiple threads share an object, if they modify the shared object at the same time, this creates a race condition.

As shown in the figure below, thread A and thread B share an object obj. Suppose thread A reads the Obj.count variable from main memory to its own CPU cache, while thread B also reads the Obj.count variable to its CPU cache, and both threads increment Obj.count by 1 operate. At this point, the operation of adding 1 to Obj.count is executed twice, but in different CPU caches.

If the two increments were performed serially, the Obj.count variable would be incremented by 2 to the original value, and the final Obj.count value in main memory would be 3. However, the two plus 1 operations in the following figure are parallel. Whether thread A or thread B flushes the calculation result to the main memory first, the Obj.count in the main memory will only increase once to 2, although there are two times in total Add 1 operation. 
write picture description here

To solve the above problem we can use java synchronized block. The synchronized code block can ensure that only one thread can enter the code competition area at the same time. The synchronized code block can also ensure that all variables in the code block will be read from the main memory. When the thread exits the code block, all variables are updated. will flush to main memory, regardless of whether the variables are volatile or not.

The difference between volatile and synchronized

For details, see  the difference between volatile and synchronized

Reference documents: 
1.  http://www.infoq.com/cn/articles/java-memory-model-1 
2.  http://www.jianshu.com/p/d3fda02d4cae

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325818074&siteId=291194637