41 Differences Between Atomic Classes and Volatile

41 Differences Between Atomic Classes and volatile #

In this lesson, we will mainly discuss the similarities and differences between atomic classes and volatile.

Case Description: Differences and Similarities between volatile and Atomic Classes #

Let’s start with a case study. As shown in the diagram below, we have two threads.

img

In the top-left corner of the diagram, we can see a shared boolean flag variable. Initially, it is set to true. Thread 2 enters a while loop and continues running or exits based on the value of this flag.

Initially, since the value of the flag is true, it will continue running for a certain period of time. Then, let’s assume that at some point, Thread 1 changes the value of this flag to false, hoping that Thread 2 will stop running after seeing this change.

However, this approach is risky. Thread 2 may not stop immediately and might stop after a certain period of time, or in the worst case scenario, it may never stop.

To understand the cause of this situation, let’s first take a look at the memory structure of the CPU. Here is a simplified diagram of a dual-core CPU:

img

As shown in the diagram, Thread 1 and Thread 2 are running on different CPU cores. Each core has its own local memory, and there is also shared memory underneath.

Initially, both threads can read the flag as true. However, when Thread 1 changes this value to false, Thread 2 cannot immediately see this modification because it cannot directly access the local memory of Thread 1. This problem is a typical visibility issue.

img

To solve this problem, all we need to do is add the volatile keyword before the variable. Once we add this keyword, every time the variable is modified, it becomes visible to other threads. So, when Thread 1 changes the value, Thread 2 can immediately see it and therefore can exit the while loop.

img

The reason why adding the volatile keyword can achieve visibility is that, after adding this keyword, the modifications made by Thread 1 will be flushed to the shared memory and then refreshed in the local memory of Thread 2. In this way, Thread 2 can sense this change. Therefore, the primary purpose of the volatile keyword is to solve visibility issues and to some extent ensure thread safety.

Now, let’s revisit a familiar scenario where multiple threads concurrently perform value++, as shown in the diagram below:

img

If it is initialized with each thread incrementing it 1000 times, the final result is likely not to be 2000. Since value++ is not atomic, thread safety issues can occur in a multi-threaded scenario. But can we solve this problem by adding the volatile keyword here?

img

Unfortunately, even if we use volatile, it cannot guarantee thread safety because the issue here is not only about visibility but also includes atomicity.

There are multiple ways to solve this problem. The first method is to use the synchronized keyword, as shown in the diagram below:

img

In this case, the two threads cannot change the value of value simultaneously, ensuring the atomicity of the value++ statement. synchronized also guarantees visibility, which means that when the first thread modifies the value of value, the second thread can immediately see the result of this modification.

The second method to solve this problem is to use atomic classes, as shown in the diagram below:

img

For example, using an AtomicInteger and having each thread call its incrementAndGet method.

After using atomic variables, there is no need to add locks. We can use the incrementAndGet method, which is ensured to be atomic by CPU instructions at the underlying level. Therefore, even if multiple threads are running simultaneously, there will be no thread safety issues.

Usage scenarios for atomic classes and volatile #

Now let’s talk about the usage scenarios for atomic classes and volatile.

We can see that the usage scenarios for volatile and atomic classes are different. If we have a visibility problem, we can use the volatile keyword. However, if our problem involves a composite operation that requires synchronization to solve atomicity issues, we should use atomic variables instead of the volatile keyword.

In general, volatile can be used to modify boolean-type flags because for flags, the assignment operation itself is atomic, and with volatile, visibility is ensured, making it thread-safe.

However, for scenarios involving counters, which are operated on by multiple threads simultaneously, the typical characteristic is that it is not just a simple assignment operation. Instead, it requires reading the current value, performing a certain modification on it, and then assigning it back. In this case, volatile is not enough to ensure thread safety. We need to use atomic classes to guarantee thread safety.