58-What are the precautions for atomic operations in Java?

What is atomicity and atomic operations

In programming, atomic operations are called atomic operations. Atomic operation refers to a series of operations, either all of them happen, or none of them happen, and there will be no situation where the execution is half terminated.

For example, the transfer behavior is an atomic operation. The process includes a series of operations such as deducting the balance, generating the transfer record by the banking system, and increasing the counterparty's balance. Although the whole process contains multiple operations, because this series of operations are combined into one atomic operation, they are either all executed successfully or not executed at all, and there will be no half of the execution. For example, my balance has been deducted, but the other party's balance does not increase. This situation will not happen, so the transfer behavior is atomic. And atomic operations that are atomic are naturally thread-safe.

Below we give an example that is not atomic. For example, when the line of code i++ is executed in the CPU, it may change from a line of code to the following 3 instructions:

  • The first step is to read;

  • The second step is to increase;

  • The third step is to save.

This shows that i++ is not atomic, and it also proves that i++ is not thread-safe, as introduced in class 06. Let's briefly review how the thread insecurity problem occurs, as shown below:

Insert picture description here
Let's look at the arrow points in turn. Thread 1 first gets the result of i=1, and then performs the i+1 operation, but assuming that the result of i+1 has not had time to be saved at this time, thread 1 is switched away, so The CPU starts to execute thread 2, what it does is the same i++ operation as thread 1, but now we think about it, what is the i it gets? In fact, the result of i is the same as the result of thread 1. It is also 1. Why? Because thread 1 performs the +1 operation on i, but the result is not saved, thread 2 cannot see the modified result.

Then suppose that after thread 2 performs the +1 operation on i, it switches to thread 1, let thread 1 complete the unfinished operation, that is, save the result 2 of i+1, and then switch to thread 2 to complete the save of i=2 Operation, although both threads have performed the operation of +1 to i, the result is that i=2 is finally saved instead of i=3 as we expected. In this way, a thread safety problem occurs and the data result is wrong. This It is also the most typical thread safety issue.

What are the atomic operations in Java

After understanding the characteristics of atomic operations, let's take a look at which operations in Java are atomic. The following operations in Java are atomic and belong to atomic operations:

  • The read/write operations of basic types (int, byte, boolean, short, char, float) except long and double are naturally atomic;

  • All read/write operations that reference the reference;

  • After adding volatile, read/write operations of all variables (including long and double). This means that after adding the volatile keyword to long and double, the read and write operations on them are also atomic;

  • Some methods of some classes in the java.concurrent.Atomic package are atomic, such as the incrementAndGet method of AtomicInteger.

The atomicity of long and double

Earlier, we described that long and double are not the same as other basic types. They don't seem to have atomicity. What is the reason for this? The official documents describe the above problems as follows:

Non-Atomic Treatment of double and long

For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.



Writes and reads of volatile long and double values are always atomic.



Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.



Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.



Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.

From the JVM specification just now, we can know that long and double values ​​need to occupy 64-bit memory space, and the writing of 64-bit values ​​can be divided into two 32-bit operations.

In this way, the assignment operation that was originally a whole may be split into two operations of the low 32-bit and the high 32-bit. If other threads read this value between these two operations, an incorrect and incomplete value may be read.

JVM developers can freely choose whether to implement 64-bit long and double read and write operations as atomic operations, and the specification recommends that JVM implement them as atomic operations. Of course, JVM developers also have the right not to do so, which is also in compliance with the specification.

The specification also stipulates that if long and double are modified with volatile, then the read and write operations must be atomic. At the same time, the specification encourages programmers to use the volatile keyword to control this problem. As the specification stipulates that for volatile long and volatile double, the JVM must guarantee the atomicity of its read and write operations, so after adding volatile, it is not for programmers. In other words, you can ensure that the program is correct.

In actual development

At this point, you may have questions. For example, if you didn't know the above issues very well before, and did not add volatile to long and double in the development process, it seems that there have been no problems? Moreover, in the future development process, is it safe to add volatile to long and double?

In fact, in actual development, it is very rare to read "half variable". This situation does not appear in the current mainstream Java virtual machines. Because the JVM specification does not force the virtual machine to implement the long and double variable write operations as atomic operations, it is actually "strongly recommended" for the virtual machine to implement the operations as atomic operations.

In the current implementation of mainstream virtual machines under various platforms, almost all read and write operations of 64-bit data are treated as atomic operations. Therefore, we generally do not need to avoid reading "half variable" when writing code. Declare long and double as volatile.

Atom Manipulation + Atom Manipulation! = Atom Manipulation

It is worth noting that simply combining atomic operations together does not guarantee that the whole is still atomic. For example, the operation behavior of two consecutive transfers cannot be combined as an atomic operation. Although each transfer operation is atomic, the combination of two transfers into one operation is not atomic, because Some other operations may be inserted between the two transfers, such as automatic system deduction, etc., resulting in the failure of the second transfer, and the failure of the second transfer will not affect the success of the first transfer.

Guess you like

Origin blog.csdn.net/Rinvay_Cui/article/details/111058807