58 Attention Points of Atomic Operations in Java

58 Attention Points of Atomic Operations in Java #

In this lesson, we mainly explain the atomicity and atomic operations in Java.

What is atomicity and atomic operations? #

In programming, an operation that has atomicity is called an atomic operation. Atomic operation refers to a series of operations that either happen completely or nothing happens at all, without the possibility of being terminated halfway through.

For example, a transfer of funds is an atomic operation which includes deducting the balance, generating a transfer record in the bank system, and increasing the balance of the recipient. Although this process involves multiple operations, they are combined into a single atomic operation. Therefore, either all these operations are executed successfully, or none of them are executed, without the possibility of executing partially. For instance, it is not possible for my balance to be deducted but the recipient’s balance not being increased. Hence, the transfer of funds possesses atomicity. Atomic operations that have atomicity inherently possess thread-safe characteristics.

Now let’s take an example that does not have atomicity, such as the line of code “i++”. When this code is executed in the CPU, it may be transformed into the following 3 instructions:

  • The first step is to read;
  • The second step is to increment;
  • The third step is to save.

This indicates that “i++” does not have atomicity, and it also proves that “i++” is not thread-safe, as mentioned in Lesson 06. Now let’s briefly review how thread-unsafe problems occur, as shown below:

img

Let’s follow the arrows in the diagram. First, Thread 1 obtains the result of i=1, then performs the i+1 operation. However, let’s assume that before the result of i+1 is saved, Thread 1 is switched away, and the CPU starts executing Thread 2, which performs the same i++ operation. Based on this, what value of i will Thread 2 get? In fact, it will get the same result as Thread 1, which is also 1. Why is this? Although Thread 1 performs the +1 operation on i, the result is not saved, so Thread 2 cannot see the modified result.

Now let’s assume that after Thread 2 performs the +1 operation on i, it switches back to Thread 1 to complete the unfinished operation, which is to save the result of i+1 as 2. Then it switches back to Thread 2 to complete the saving operation of i=2. Although both threads performed the +1 operation on i, the final result saved is i=2, not the expected i=3. This is where thread safety problems occur, resulting in incorrect data results. This is also the most typical thread safety problem.

What are the atomic operations in Java? #

After understanding the characteristics of atomic operations, let’s take a look at the atomic operations in Java. The following Java operations have atomicity and are considered atomic operations:

  • Read/write operations of basic types (int, byte, boolean, short, char, float) other than long and double inherently possess atomicity;
  • Read/write operations of all reference types possess atomicity;
  • Read/write operations of all variables (including long and double) possess atomicity once the “volatile” keyword is added;
  • Certain methods of some classes in the java.concurrent.Atomic package are considered atomic operations, such as the “incrementAndGet” method in AtomicInteger.

Atomicity of long and double #

In the previous section, we mentioned that long and double types are different from other primitive types and may not have atomicity. What is the reason for this? The official documentation describes the issue 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 mentioned earlier, we can know that long and double values occupy 64 bits of memory space, and for a 64-bit value, it can be divided into two 32-bit operations for writing.

As a result, a single assignment operation can be split into two operations: one for the lower 32 bits and the other for the higher 32 bits. If another thread reads the value between these two operations, it may read an incorrect or incomplete value.

The developers of JVM have the freedom to choose whether to implement the read and write operations of long and double values 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, and that is also in compliance with the specification.

The specification also states that if long and double are modified with the volatile keyword, their read and write operations must have atomicity. At the same time, the specification encourages programmers to use the volatile keyword to control this issue. Since the specification requires the JVM to ensure the atomicity of read and write operations for volatile long and volatile double, adding the volatile keyword ensures correctness for programmers.

In Practical Development #

At this point, you may have questions such as: if you previously had little understanding of the above issues and did not use volatile for long and double in the development process, it seems that there were no problems? And in the future development process, is it necessary to use volatile for long and double to ensure safety?

In practical development, it is extremely rare to read “half a variable”. This situation does not occur in the mainstream Java virtual machines currently in use. Although the JVM specification does not require the virtual machine to implement the write operations of long and double variables as atomic operations, it strongly recommends that the virtual machine do so.

In the current implementations of virtual machines on various platforms, almost all of them treat read and write operations on 64-bit data as atomic operations. Therefore, when writing code, we generally do not need to declare long and double as volatile in order to avoid reading “half a variable”.

Atomic Operation + Atomic Operation ≠ Atomic Operation #

It is worth noting that simply combining atomic operations together does not guarantee the overall atomicity. For example, the behavior of making two consecutive money transfers cannot be combined as a single atomic operation. Although each individual transfer operation is atomic, combining the two transfers into one operation does not have atomicity. This is because there may be other operations, such as automatic deduction of fees, inserted between the two transfers, resulting in the second transfer failure, but it does not affect the success of the first transfer.

That concludes the content of this lesson. We have introduced what atomicity is, the atomic operations in Java, and provided detailed explanations of the special case of long and double. Finally, we also mentioned that simply combining atomic operations together does not guarantee overall atomicity.