Concurrency based Java memory model JMM

Table of contents

foreword

thread communication

memory model

reorder

Three elements of concurrent programming

1. Atomicity

2. Visibility

3. Sequence

thread safety

1. Guaranteed atomicity

2. Ensure visibility

3. Guaranteed sequence

Remark

as-if-serial semantics

happens-before principle


foreword

In our actual work and development scenarios, multithreading is often used to improve the operating efficiency of the system. For multi-threaded coding, we will use many programming APIs in the Java toolkit, including not only thread communication methods such as wait(), join(), notify(), but also many such as CountDownLatch, ReentrantLock, etc. Concurrent tool classes, through these tool classes and methods, we can write multi-threaded business rules arbitrarily to achieve the results we want. So, have children's shoes thought about why these methods and tool classes can ensure normal access to memory by multiple threads? The answer is today's protagonist - the Java memory model.

thread communication

When it comes to the Java memory model, we have to talk about the thread communication method first. There are two thread communication methods, one message communication mechanism, which is the so-called message sending to ensure the communication between two or more threads; the other is the shared memory method, that is, multiple threads share the common memory, and each The operation fetches of each thread are based on the common memory. The operation of the heap in our JVM is based on the second method, which is shared memory, which is why we want to discuss the Java memory model.

memory model

The English of the Java memory model is Java Memory Model, or JMM for short. Objects, static variables, and string constant pools in java are all stored in the heap memory. Local variables in other methods and objects that have not escaped after escape analysis are stored in the stack area, and the data stored in the heap area has Memory visibility and other issues, and the JMM memory model is to solve the communication between threads, that is, when a thread modifies a shared memory variable is visible to another thread. The JMM memory model defines the abstract relationship between multithreading and shared variables: all shared variables in a thread are stored in the main memory, that is, in the heap memory, and each thread has its own local memory, which contains pairs of shared variables. Copy copy, and local memory does not really exist is just an abstract model. In the actual multi-threaded operation, the thread will first copy the variable copy from the main memory when operating on the shared variable. When the operation is completed, the variable will be written back to the main memory. All threads can only communicate through the main memory.

reorder

Everyone knows that Java classes need to be compiled into JVM executable class files through the compiler. After the JVM is loaded-verified-prepared-parsed-initialized and then run, it will call the computer processor to operate. The compilation stage and the processor stage are guaranteed. The efficiency of program operation will have the situation of instruction reordering. However, although reordering can provide efficiency, it will cause multi-thread safety problems in some special cases. Therefore, JMM's compiler reordering rules will prohibit specific types of compiler reordering, and processor reordering rules will insert memory barriers during compiler compilation to prohibit instruction reordering. Of course, based on the as-if-serial semantics, the compiler and processor will not reorder the instructions that have dependencies.

Three elements of concurrent programming

1. Atomicity

In one or more operations, either the thread is not interrupted by other operations during execution, or none of them are executed.

Many places in the Java code writing process are actually non-atomic, such as:

@Test 
public void toTest(){ 
    //原子操作 
    int a = 1; 
    //非原子操作,第一步计算a的值,第二步再进行赋值 
    a = a+1; 
}

Non-atomic operations can cause multi-thread safety issues.

2. Visibility

When multiple threads operate on shared variables, the results modified by one thread should be immediately visible to other variables.

Since our JMM memory model communicates multiple thread shared variables through shared main memory, each thread maintains a local memory copy by itself. If there is a local memory modification variable that does not refresh the shared main memory data in time, the variable data obtained by other threads will be wrong data, which will lead to multi-line security issues.

3. Sequence

The sequence of program execution should follow the code sequence. In the reordering environment allowed by JMM, the execution result of a single thread is consistent with that without reordering.

In a Java program, if within this thread, all operations can be regarded as orderly. In a multi-threaded environment, one thread observing another thread is regarded as out of order. In this blog, we describe instruction reordering. Although reordering can improve operating efficiency, it sometimes disrupts the order of code execution and instruction execution. Different execution orders of code and instructions may also be different in multi-threaded situations. There are different execution results.

thread safety

Above we introduced that the JMM memory model is an abstract relationship between multithreading and shared variables. We learned that Java compilers and computer processors will rearrange the code order and instructions to improve efficiency. We also learned that multithreaded programming has many security issues. . So, how does the JMM memory model solve multithreading problems?

1. Guaranteed atomicity

1.1 Self-contained atomicity

The reading and assignment of basic data types in java are atomic by default, such as: byte, short, int

1.2 Atomic Class Operations

The JUC java.util.concurrent.atomic package provides a lot of atomic operations, which can guarantee the atomicity of operations, such as: AtomicLong, AtomicInteger, etc.

1.3 synchronized

The synchronized synchronization class can ensure that the thread entering the method and class is single, thereby ensuring the serialization of the execution process to ensure the atomicity of the execution result.

1.4 Lock mechanism

The bottom layer of the lock mechanism is implemented by AQS. Whether it is a fair lock, an unfair lock, or a read-write lock, it basically guarantees the atomicity of the execution result.

2. Ensure visibility

2.1 synchronized

The synchronized synchronization class can ensure that the thread entering the method and class is single. Its essence is that the latest variable data will be loaded from the main memory when entering the synchronization party and or class. When the thread execution is completed, the local memory will be flushed into the main memory, thus ensuring visibility.

2.2 Lock lock mechanism

The bottom layer of the lock mechanism is implemented by AQS. Whether it is a fair lock, an unfair lock, or a read-write lock, the latest shared memory data can be obtained. The principle is similar to that of synchronized.

2.3 Volatile keyword

The role of the Volatile keyword is that when a thread modifies a local memory variable, it will be flushed into the main memory immediately, and the copy of the variables of other threads will be invalid immediately, and other threads need to read and write this variable and must reload it from the main memory.

3. Guaranteed sequence

3.1 synchronized

The synchronized synchronization block and method guarantee the serialization of execution threads. We can think that the operations in a single thread are all in order.

3.2 Lock lock mechanism

The Lock lock mechanism is the same as the synchronization code block. Only a single thread is allowed to perform operations, and of course the sequence of operations can be guaranteed.

3.3 Volatile keyword

The bottom layer of the Volatile keyword uses memory barriers to prohibit instruction rearrangement, so that operations in a multi-threaded environment are ordered.

3.4 happens-before principle

The happens-before principle is the partial order relationship between two operations defined in the Java memory model. If operation A occurs before operation B, that is to say, before operation B occurs, the impact of operation A can be observed by operation B.

Remark

as-if-serial semantics

No matter how reordered, the execution result of the program cannot be changed. The compiler, runtime, and processor must all obey as-if-serial semantics. In order to comply with as-if-serial semantics, the compiler and processor will not reorder operations with data dependencies, because such reordering will change the execution result. However, operations may be reordered by compilers and processors if no data dependencies exist between them.

happens-before principle

In JMM, if the result of an operation needs to be visible to another operation, there must be a happens-before relationship between the two operations. The two operations mentioned here can be within one thread or between different threads. There is a happens-before relationship between two operations, which does not mean that the former operation must be executed before the latter operation. happens-before only requires that the previous operation (the result of the execution) be visible to the latter operation, and that the previous operation is in order before the second operation.

The happens-before rule is as follows:

Program Order Rule: Every operation in a thread happens-before any subsequent operation in that thread.

Monitor Lock Rule (Monitor Lock Rule): The unlocking of a lock happens-before the subsequent locking of the lock.

Volatile Variable Rule: A write to a volatile domain happens-before any subsequent read of the volatile domain.

start() rule (Thread Start Rule): If thread A executes thread B.start() (start thread B), then A thread's B.start() operation happens-before any operation in thread B.

join() rule (Thread Join Rule): If thread A executes thread B.join() and returns successfully, then any operation in thread B happens-before thread A successfully returns from B.join() operation.

Program Interruption Rule (Thread Interruption Rule): The call to thread interrupt() happens-before the interrupted thread's interrupted() or isInterrupted().

finalizer rule (Finalizer Rule): The end of an object constructor happens-before the beginning of the object finalizer ().

Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.

Guess you like

Origin blog.csdn.net/weixin_39970883/article/details/129494720