Atomicity, visibility and ordering in Java concurrency

Based on the JMM memory model, the core issues of Java concurrent programming: atomicity, visibility, and orderliness

So before that, we need to talk about Java's JMM memory model: The Java memory model is a working mode of the Java virtual machine specification, which divides memory into main memory and working memory. When a thread operates a variable, it copies the data in the memory to the working memory. After the operation is completed in the working memory, it writes it back to the main memory.

What does the Java memory model do?

It defines how threads communicate through memory and how to synchronize to ensure program correctness; the Java memory model stipulates that the data read by one thread is the latest version written by another thread. Because multiple threads can access shared variables at the same time in a concurrent program, if measures are not taken to avoid unexpected results caused by concurrent thread access, the program may have various unknown problems, such as data inconsistency, deadlock, etc., and Java memory Models are designed to solve these problems.

The variable data is stored in the main memory. When the thread operates the variable, it will copy the data in the main memory to the working memory. After the operation is completed in the working memory, it will be written back to the main memory.

Note: The local memory here is just an abstraction of JMM. It is a virtual concept. It does not actually exist. A piece of memory is called local memory. It is a method used to describe the visibility, orderliness and atomicity rules between Java threads. specification.

1.Atomicity

Atomic means indivisible, which means that when performing a series of operations , these operations are either all executed or not executed, and there is no situation where only a part is executed.

What do the above series of operations specifically refer to?

The answer is those read and write operations on shared variables. In fact, this also involves the core of the entire concurrent programming: dealing with competition conditions between threads and access to shared variables in multi-threaded situations.

We all know that concurrency refers to the alternate execution of multiple threads in the same time period from a micro perspective, and from a macro perspective, multiple threads execute together. This is because our current CPU is multi-core and based on the JMM memory model. As a result, we always face such a problem: multiple threads perform read and write operations on the same shared variable at the same time . How can we keep the results of these operations correct? In fact, we need to maintain these three properties: Atomicity, visibility, order. How to maintain these three characteristics, I think this is the problem to be solved in this chapter of concurrent programming.

Why keep it atomic?

Because when multiple threads access the same shared variable at the same time, if atomicity is not guaranteed, thread safety issues may occur.

For example, if multiple threads accumulate a counter variable at the same time, if atomicity is not guaranteed, the counting results may be incorrect. In order to avoid this situation, you can define the type of the counter variable as AtomicInteger, or use the synchronized keyword to lock the read and write operations to ensure that when multiple threads operate on the counter variable, only one thread is operating at a time, thus Atomicity is guaranteed.


How to maintain atomicity?

1. Lock: Use Synchronized or ReentrantLock

We lock part of the code that reads and writes shared resources . After one thread obtains the lock, when another thread wants to access this code, the thread will be blocked until the other thread releases the lock; so locking is a blocking implementation. And this is the realization of a pessimistic lock idea.

2. Use atomic classes for concurrent operations

In java, some atomic classes in the java.util.concurrent package are also provided; it is a lock-free implementation; it is used in low concurrency situations ; it adopts the CAS mechanism (Compare-And-Swap).

In short, the atomicity of atomic classes realizes atomic operations through volatile + CAS. For example, in the AtomicInteger class, the value in the AtomicInteger class is modified with the volatile keyword, which ensures the memory visibility of the value, which provides the basis for subsequent CAS implementation.

CAS mechanism (more important, high frequency of interviews)

CAS mechanism (Compare-And-Swap): Compare and swap, this algorithm is hardware support for concurrent operations;

It is an implementation method of optimistic locking; it adopts the idea of ​​spin and is a lightweight locking mechanism (optimistic locking is a lock-free implementation).

The full name of CAS is compare and swap, which is an optimistic locking idea, that is, a lock-free implementation. This algorithm is hardware support for concurrent operations;

Recommended for use in low concurrency situations,

The main idea is the spin idea : take the memory value v into the working memory for the first time, perform calculations to get the updated value B, and then make a judgment, take out V again from the main memory, and record it as A; if and only if A== V, we can update B back to the main memory. Otherwise, it means that a thread has changed it before, and we will use this changed value to repeat the process just now.

Features:

  • It is a lock-free implementation;

  • Can only be used under low concurrency conditions;

  • Without locking, all threads can operate on shared data;

  • Since it is not locked, it will not block and is more efficient than locking;

  • Adopt spin thinking;

The continuous spinning (spinlock) in the CAS mechanism means that when multiple threads request to operate the same variable at the same time, if the value of the variable does not meet the thread's expectations, the thread will repeatedly try to re-calculate the value until it succeeds. , instead of giving up processing or going to sleep.

The advantage of using the CAS mechanism is that there is no lock overhead. Since there is no overhead of context switching between threads, the performance is higher than using the lock mechanism. But at the same time, there may also be problems with increased spin waiting (such as too many threads), resulting in performance degradation. Therefore, proper tuning is required when using CAS

 2. Visibility

Visibility means that the value of a shared variable modified by one thread can be seen by other threads in a timely manner. When one thread modifies the value of a shared variable and other threads are still using this value, if other threads still see the old value before the modification, it will cause a program error or run abnormality.

How to maintain visibility?

Properly ensuring visibility is an important topic in concurrent programming. To solve the visibility problem, Java provides a variety of solutions:

  1. Locking synchronization mechanism: Through locking synchronization mechanisms such as the synchronized keyword or the ReentrantLock class, you can ensure that the read and write operations of shared variables are atomic, visible, and orderly.

  2. volatile keyword : Using the volatile keyword can ensure that the modified variable is visible to all threads, and the read and write operations on the shared variable are atomic.

  3. final keyword: Variables modified by the final keyword are guaranteed to be visible in all threads, but only apply to immutable variables.

  4. Atomic Variables: The Atomic package in Java provides efficient cross-multithreaded operations on primitive data types with guarantees of visibility and atomicity.

With volatile ensuring that changes to variables are immediately visible, does it eliminate the need for locking?

Although the volatile keyword can ensure the visibility of variables and a certain orderliness, it is not equivalent to the locking synchronization mechanism and cannot be used to replace the locking synchronization mechanism.

Lock synchronization mechanisms (such as using synchronized methods or code blocks, ReentrantLock classes, etc.) can not only ensure visibility and orderliness, but also ensure that operations on shared variables by multiple threads are atomic, thus avoiding some other problems. Thread safety issues, and using volatile to modify shared variables can only guarantee visibility and a certain orderliness, but cannot guarantee the atomicity of composite operations.

In short, although the volatile keyword can provide visibility and ordering guarantees, it cannot replace the locking synchronization mechanism. Developers need to choose an appropriate synchronization mechanism based on the actual situation to ensure thread safety.

3. Orderliness

Orderliness means that the program is executed in the order of the code.

In order to optimize performance, sometimes Java will automatically adjust and rearrange the execution order of some code instructions to improve speed. In some cases, after the order is adjusted, subsequent code operations may be affected.

How to ensure orderliness?

For variables modified with the volatile keyword , the code related to it will not be reordered during execution. Can solve disorder problems ;

Summary: The volatile keyword can guarantee ordering and visibility, but the guarantee of atomicity is more complicated. There are two ways to ensure atomicity, locking and no locking. As long as we maintain these three properties for concurrent operations on shared variables, we can maintain the correctness of the operations. This is the real purpose of Java concurrent programming!

Guess you like

Origin blog.csdn.net/weixin_52394141/article/details/131330158