[The principle of multithreading and high concurrency: 3_java memory model]

1 Overview

The Java Memory Model is the Java Memory Model, or JMM for short. From an abstract point of view, JMM defines the shared variables between threads and main memory,  抽象关系 the shared variables between threads are stored in main memory, each thread has a private, and the  工作内存 working memory stores the thread to read/write shared variables  副本 . Working memory is an abstraction of the JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations.

The Java memory model is similar to the cpu cache model. The Java memory model is established based on the cpu cache model, but the Java memory model is standardized, shielding the difference between different computers at the bottom.

2. Problems caused by Java memory model

The Java memory model specifies the operations of threads on main memory  原子性 , including the following eight operations:

lock: main memory, which identifies the variable as exclusive to the thread;

unlock: main memory, unlock thread exclusive variables;

read: main memory, read memory to thread cache (working memory);

load: working memory, the value after read is put into the thread local variable copy;

use: working memory, pass the value to the execution engine;

assign: working memory, the execution engine results are assigned to thread local variables;

store: working memory, save the value to the main memory for write standby;

write: main memory, write variable values.

Suppose the following program, two threads without synchronization control go to increment i at the same time, what will happen?

public class Test {
    private int i = 0;
    public void increment() {
        i++;
        System.out.println("i=" + i);
    }

    public static void main(String[] args) {
        Test t = new Test();
        new Thread(() -> t.increment()).start();
        new Thread(() -> t.increment()).start();
    }
}

By running, the following three situations will occur

i=1
i=1

or

i=1
i=2

or

i=2
i=2

The first case is explained by the figure below

Both threads A and B have their own working memory. Since A executes the read operation, it reads i=0 from the main memory, and then the load operation loads its own working memory, and then executes the use operation to auto-increment i, and then From the new assignment operation assign, the working memory of thread A is i=1, then the store operation is stored, and finally it is written back to the main memory, and finally i=1.

Thread B also does this, read->load->use->assign->store->write, and finally i=1.

The second type, the key is that the read operation of the B thread is refreshed from the A thread to the main memory before the value is obtained. The execution sequence is: thread A increments -> thread A prints the final value of i -> thread B increments -> thread B prints the final value of i, as shown below

The third type is that thread A refreshes i=1 to the main memory after self-increasing. Before executing printing, thread B first obtains i=1 from the main memory, and performs read->load->use->assign->store ->write, increment i=1 to i=2, and then thread A performs the printing operation. The execution order is: thread A increments -> thread B increments -> thread A prints the final value of i -> thread B prints i The final value, as shown below

3. Visibility, order, atomicity

Although the java memory model JMM provides each thread with each working memory and stores the variable copy of the shared variable, if the thread does not control the visibility, it can be seen from the above process that the modification of the shared variable under multi-threading , the results are still unpredictable.

3.1 Visibility

The volatile keyword, at the program level, ensures that changes to a shared variable are immediately visible to other threads. The above program adds the volatile keyword to i to ensure that the second result is always obtained.

The following program is used to demonstrate:

Class VolatileExample {
	int  a = 0;
	volatile boolean flg = false;
	
	public void writer() {
		a = 1;
		flg = true;
	}
	
	public void reader() {
		if (flg) {
			int i = a;
			......
		}
	}
}

The diagram is as follows:

The above process can be summed up in two sentences:

当写一个volatile修饰的变量时,JMM会把线程对应的本地内存中的共享变量值刷新的主内存;
当读一个volatile修饰的变量时,JMM会把该线程对应的本地内存置为无效,从主内存读取最新的共享变量的值。

The above procedure explains the visibility problem of volatile.

3.2 Orderliness

For some code, the compiler or processor, in order to improve the efficiency of code execution, will reorder the instructions, for example, the following code:

flg = false;
//Thread 1:
parpare(); // prepare the resource
flg = true;

//Thread 2:
while(!flg) {
	Thread.sleep(1000);
}
execute();// Execute the operation based on the prepared resource

After reordering, let flag = true execute first, which will cause thread 2 to skip the while waiting directly and execute a certain piece of code. As a result, the prepare() method has not been executed, and the resources are not ready. At this time, the code logic will be caused. Abnormal.

Volatile passes the memory barrier to ensure that volatile-modified variables, and their values ​​defined before and after, do not have instruction rearrangement. JMM defines the following four memory barriers StoreStore, StoreLoad, LoadLoad, LoadStore;

For volatile write, insert StoreStore in the front, prohibit the reordering of the normal read above and the volatile write below; insert StoreLoad later, prohibit the reordering of the volatile write above and the normal read below, as shown in the following figure:

For volatile read, insert LoadLoad later, prohibit the reordering of the above volatile read and the normal read below; insert LoadStore next, prohibit the reordering of the above volatile read and the normal write below, as shown below:

happens-before principle

In order to ensure that instruction rearrangement must not occur between multiple threads in some cases, the Java memory model stipulates 8 principles.

  1. Program order rule: In a thread, according to the code order, the operation written in front occurs first before the operation written in the back;

  2. Monitor locking rules: an unLock operation occurs first before a subsequent lock operation on the  same  lock;

  3. volatile variable rule: a write operation to a variable occurs first before a subsequent read operation of the variable;

  4. Thread start rules: The start() method of the Thread object occurs first for each action of this thread;

  5. Thread termination rule: All operations in the thread occur first in the thread termination detection. We can detect that the thread has terminated execution through the end of the Thread.join() method and the return value of Thread.isAlive().

  6. Thread interrupt rules: The call to the thread interrupt() method occurs first when the code of the interrupted thread detects the occurrence of the interrupt event;

  7. Object finalization rules: the initialization of an object occurs first at the beginning of its finalize() method;

  8. Transitivity: If operation A occurs before operation B, and operation B occurs before operation C, it can be concluded that operation A occurs before operation C;

3.3 Atomicity

In general, volatile-modified variables cannot guarantee atomicity. For example, i++ is a compound operation. It is not atomic to read first and then modify the value of the variable.

4. The role of volatile

From the above description, it can be concluded that there are two main functions of volatile:

  • Guaranteed thread visibility
  • Disable instruction reordering

5. HotSpot level implementation

View the java assembly file through the hsdis tool, first download hsdis-amd64.dll to \jdk1.8\jre\bin, then set the VM parameters, -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

In the final execution, the following information will be added before the volatile variable

lock addl $0x0,(%rsp)

As shown below:

6. Low-level CPU hardware implementation

In the above process, the JVM virtual machine  lock前置指令 will send the data to the CPU to write the cache line data where this variable is located back to the main memory. If the value cached by other CPUs is the old value, there will be a problem. In multi-CPU (here refers to multiple cores) , each CPU will  嗅探 check whether the data propagated on the bus is consistent with its own cache,  缓存一致性协议 and finally ensure the consistency of the internal cache data of multiple CPUs, which is illustrated by the figure below.

The lock prefix instruction of the virtual machine is completed by the cache coherence protocol in the underlying hardware. Different CPU cache coherence protocols are different. There are MSI, MESI, MOSI, Synapse, Firefly and Dragon, and the cache coherence protocol of Intel CPU. It is done through MESI.

In order to implement the MESI protocol, two technical terms need to be explained:  flush处理器缓存 ,  refresh处理器缓存 .

Flush the processor cache, he means to flush the updated value to the cache (or main memory), because it must be flushed to the cache (or main memory), it is possible to pass some special The mechanism allows other processors to read updated values ​​from their own cache (or main memory). In addition to flush, he will also send a message to the bus (bus) to notify other processors that the value of a variable has been modified by him.

Refresh the processor cache, which means that when a thread in the processor reads the value of a variable, if it finds that threads of other processors have updated the value of the variable, it must retrieve the value of the variable from the cache of other processors (or main memory), read this latest value and update it into its own cache. Therefore, in order to ensure visibility, the bottom layer is guaranteed by the MESI protocol, the flush processor cache and the refresh processor cache, this whole set of mechanisms.

Flush and refresh, these two operations, flush is to force the data to be flushed to the cache (main memory), not just stay in the write buffer; refresh is to sniff the bus and find that a variable has been modified, and must be forced from other processing The cache (or main memory) of the server loads the latest value of the variable into its own cache.

7. Summary

This article mainly describes the role of the Java memory model, shielding the details of the underlying implementation, and at the same time bringing a series of problems, leading to three major problems between threads, namely orderliness, visibility, atomicity, and volatile keyword modification The role of the variables in multi-threading, and a preliminary analysis of how the bottom layer is implemented. If you want to analyze it in depth, this depends on the MESI protocol specification and the implementation logic of the bottom layer of different hardware, such as Intel's operation manual. There is time later go deeper

Guess you like

Origin blog.csdn.net/Trouvailless/article/details/124389885