Atomicity, visibility, and orderly solutions

Atomicity, visibility, and orderly solutions

(1) Atomicity

Atomicity means that one or more operations are either all executed and not interrupted by any factor during execution, or all are not executed. In Java, when we discuss the atomicity of an operation, it generally means that the operation will be interrupted by the random scheduling of threads.

JMM's guarantee of atomicity is roughly divided into the following types: java comes with atomicity, synchronized, Lock, and atomic operation class (CAS). Let us elaborate one by one below.

1. Java comes with atomicity

In Java, the read and assign operations of variables of basic data types are atomic operations, but the long and double types are 64-bit. In the 32-bit JVM, the read and write operations of 64-bit data will be divided into two 32-bit operations. Processing, so long and double are non-atomic operations in the 32-bit JVM, which means that they are not thread-safe for concurrent access.

In particular, note that what we are talking about here is only the atomicity of read and assignment. The assignment here only refers to the assignment of specific values, and does not include the assignment of variables to variables. In addition, combined operations (such as ++ and-operations) are also not atomic. We can look at a few classic examples:

a = true;  // 原子性
a = 5;     // 原子性
a = b;     // 非原子性,分两步完成,第一步加载b的值,第二步将b赋值给a
a = b + 2; // 非原子性,分三步完成
a++;      // 非原子性,分三步完成

Friends who know assembly language can easily understand why the following three examples cannot be completed in one step. Friends who don’t know assembly language can remember, this place is very important.

2. synchronized

Synchronized can guarantee the atomicity of the operation result (note the description here). The principle of synchronized to ensure atomicity is also very simple, because synchronized can prevent multiple threads from executing the same piece of code concurrently.

After the synchronized method is added, when one thread finishes executing the method, other threads cannot execute this code. In fact, we found that synchronized does not turn the code into an atomic operation. The code may still be interrupted during execution. However, even if it is interrupted, other threads cannot take the opportunity to suddenly enter the critical section to execute this code. When it was interrupted before The thread continues to execute until the end and the result is correct.

Therefore, the guarantee of synchronized to the atomicity problem is guaranteed from the final result, that is, it only guarantees that the final result is correct, and there is no guarantee whether the intermediate operation is interrupted.

3. Lock

The principle of Lock is basically the same as that of synchronized, so I won't repeat it.

4. Atomic operation class (CAS)

JDK provides a lot of atomic operation classes to ensure the atomicity of operations. The bottom layer of the atomic operation class uses the CAS mechanism, which guarantees atomicity and is essentially different from synchronized. The CAS mechanism ensures that the entire assignment operation is atomic and cannot be interrupted, while synchronized can only guarantee the correctness of the final execution result of the code, which means that synchronization can eliminate the impact of atomic problems on the final execution result of the code, but the atomic operation class (CAS) truly guarantees the atomicity of the operation.

(2) Visibility

The Java memory model stipulates that all variables are stored in the main memory. Each thread has its own working memory. The value stored by the thread in the working memory is a copy of the value in the main memory. All operations on the variable by the thread must be in It is performed in the working memory, instead of directly reading and writing the main memory, the latest value of the variable will be refreshed back to the main memory after the thread has finished operating the variable.

But when to refresh this latest value is random. So it is possible that a thread has updated a shared variable, but has not flushed it back to the main memory, then other threads that read and write this variable will not see the latest value. Another possibility is that although the modification thread has flushed the latest value to the main memory, but the cache value of the copy in the working memory of the reader thread has not expired, then the reader thread will still use the copy value instead of the main memory The latest value of. This is the visibility problem in a multi-CPU multi-threaded programming environment.

JMM proposes the following solutions to the visibility problem: volatile, synchronized, Lock, and atomic operation (CAS), which will be detailed below.

1. volatile

We can look at what volatile does. Using volatile to modify a shared variable can achieve the following effects (memory semantics):

  1. Once the thread modifies the copy of this shared variable, it will immediately refresh the latest value to the main memory
  2. Once a thread modifies the copy of this shared variable, the value of the copy of this shared variable in other threads will become invalid. If other threads need to read and write this shared variable, they must reload it from the main memory.

The bottom layer of volatile uses a memory barrier to ensure visibility. Let's first understand the memory barrier.

Memory barrier (English: Memory barrier), also known as memory barrier, memory barrier, barrier instruction, etc., is a type of synchronization barrier instruction, which is a synchronization point of the CPU or compiler in the operation of random access to memory, making this point After all the previous read and write operations have been executed, the operations after this point can be started. Most modern computers implement out-of-order execution in order to improve performance, which makes memory barriers necessary. Semantically, all write operations before the memory barrier must be written to the memory; read operations after the memory barrier can obtain the result of the write operation before the synchronization barrier. Therefore, for sensitive program blocks, memory barriers can be inserted after write operations and before read operations.

We can extract two points from the above large definition to correspond to the memory semantics of volatile: write operations before the memory barrier must be flushed back to main memory immediately, and read operations after the memory barrier must read the latest value from the main memory. . This is the basic principle that volatile guarantees visibility.

2. synchronized

When the thread releases the lock, JMM flushes the shared variables in the local memory corresponding to the thread to the main memory. When a thread acquires a lock, JMM invalidates the local memory corresponding to the thread, so that the critical section code protected by the monitor must read shared variables from the main memory. This ensures visibility.

From here, we find that locks actually have the same memory semantics as volatile, so the use of synchronized can also achieve visibility of shared variables.

3. Lock

The principle of Lock is basically the same as that of synchronized, so I won't repeat it.

4. Atomic operation class (CAS)

The use of atomic operation classes can also ensure the visibility of shared variable operations. The underlying atomic operation class uses the CAS mechanism. The CAS mechanism in Java will get the latest value from the main memory for comparison every time, and set the new value to the main memory after the comparison is consistent. And this entire operation is an atomic operation. So every time the CAS operation gets the latest value in the main memory, the value of each set will also be written to the main memory immediately.

(3) Orderliness

The root cause of the order problem is instruction rearrangement. Instruction rearrangement refers to the reordering and execution of source code instructions by the compiler and processor without affecting the result of single-threaded execution of the code. This reordering execution is an optimization method in order to make full use of the arithmetic unit inside the processor and improve the overall operating efficiency of the program.

Insert picture description here

Reordering is divided into the following types:

  1. Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.
  2. Instruction-level parallel reordering. Modern processors use instruction-level parallel technology to overlap multiple instructions. If there is no data dependency, the processor can change the execution order of the statements corresponding to the machine instructions.
  3. Reordering of the memory system. Because the processor uses cache and read/write buffers, this makes load and store operations appear to be performed out of order.

In order to improve the performance of the program, the processor can reorder the program. However, it must be satisfied that the results of the reordered code executed in a single-threaded environment cannot be changed (very critical). This principle is what we often call as-if-serial semantics. In order to comply with the as-if-serial semantics, the compiler and processor will not reorder operations that have data dependencies, because this reordering will change the execution result. However, if there is no data dependency between operations, these operations may be reordered by the compiler and processor.

However, as-if-serial can only guarantee that the result remains unchanged in a single-threaded case, and cannot guarantee in a multi-threaded case. Therefore, the order problem under multithreading may lead to unpredictable changes in the final result, which is a very serious problem.

Therefore, JMM uses four methods to ensure order: happens-before principle, synchronized, Lock, and volatile. Let's talk about them in detail below:

1. The happens-before principle

Let's first look at the definition in "The Art of Java Concurrent Programming":

In JMM, if the result of one 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. The happens-before relationship between two operations does not mean that the previous operation must be executed before the next operation! Happens-before only requires the previous operation (the result of execution) to be visible to the next operation, and the previous operation is ordered before the second operation (the f irst is visible to and ordered before the second)

The following are some of the precedent relationships that come with the Java memory model (taken from "The Art of Java Concurrent Programming"). These precedent relationships already exist without the assistance of any synchronizer and can be used directly in coding. If the relationship between the two operations is not in this list and cannot be deduced from the following rules, they have no guarantee of sequence, and the virtual machine can reorder them at will:

  1. Program Order Rule: In a thread, in accordance with the order of the program code, the operations written in the front occur first in the operations written in the back. To be precise, it should be the control flow sequence rather than the program code sequence, because branches, loops and other structures must be considered.
  2. Monitor Lock Rule (Monitor Lock Rule): An unlock operation occurs before the lock operation on the same lock. What must be emphasized here is the same lock, and "behind" refers to the sequence of time.
  3. Volatile Variable Rule (Volatile Variable Rule): The write operation of a volatile variable occurs first in the subsequent read operation of this variable. Here, "behind" also refers to the order of time.
  4. Thread Start Rule: The start() method of the Thread object precedes every action of this thread.
  5. Thread Termination Rule: All operations in a thread occur first in the termination detection of this thread. We can detect that the thread has been detected by the end of the Thread.join() method and the return value of Thread.isAlive(). Terminate execution.
  6. Thread Interruption Rule: The call to the thread interrupt() method occurs first when the code of the interrupted thread detects the occurrence of an interrupt event. You can use the Thread.interrupted() method to detect whether an interrupt occurs.
  7. Finalizer Rule: The completion of the initialization of an object (the end of the execution of the constructor) 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, then it can be concluded that operation A occurs before operation C.

If the happens-before principle cannot be met, the synchronized mechanism and volatile mechanism must be used to ensure order.

2. synchronized

Synchronized method to ensure order is very simple and rude, but this also brings greater waste of resources. Synchronized semantics means that the lock can only be acquired by one thread at the same time, and when the lock is occupied, other threads can only wait. Therefore, the synchronized semantics requires that threads can only be executed "serially" when accessing read and write shared variables, so synchronization is orderly.

3. Lock

The principle of Lock is basically the same as that of synchronized, so I won't repeat it.

4. volatile

The bottom layer of volatile uses memory barriers to ensure orderliness. If volatile is used to modify shared variables, volatile at the bottom of the JVM uses a "memory barrier" to prohibit reordering of specific types of processors. When the volatile keyword is added, there will be an extra lock prefix instruction, which is actually equivalent to a memory barrier (also a memory barrier). Memory barriers can guarantee:

  1. When the second operation is a volatile write, no matter what the first operation is, it cannot be reordered. This rule ensures that operations before volatile writes will not be reordered by the compiler to after volatile writes.
  2. When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This rule ensures that operations after volatile read will not be reordered by the compiler to before volatile read.
  3. When the first operation is a volatile write and the second operation is a volatile read, it cannot be reordered.

(4) Summary

Insert picture description here
Let's summarize the above points. In fact, there are three key points: volatile cannot guarantee atomicity, synchronized can only guarantee that atomicity issues do not affect the final result but cannot truly guarantee atomicity, and atomic classes cannot guarantee orderliness.

July 11, 2020

Guess you like

Origin blog.csdn.net/weixin_43907422/article/details/107243822