39 How Do Atomic Classes Utilize Cas to Ensure Thread Safety

39 How Do Atomic Classes Utilize CAS to Ensure Thread Safety #

This tutorial primarily explains how atomic classes use CAS to ensure thread safety.

What are atomic classes? What is their purpose? #

To answer this question, first we need to understand what atomic classes are and what their purpose is.

In the programming field, atomicity means that “a group of operations either all succeed or all fail, and cannot partially succeed”. The classes under java.util.concurrent.atomic are classes that have atomicity and can perform atomic operations such as addition, increment, and decrement. For example, the previously thread-unsafe i++ problem in multithreading can be elegantly solved using atomic classes with the functionally equivalent and thread-safe getAndIncrement method.

The purpose of atomic classes is similar to that of locks and is to ensure thread safety in concurrent situations. However, atomic classes have certain advantages over locks:

  • Finer granularity: Atomic variables can narrow down the scope of competition to the level of variables. In general, the granularity of locks is greater than that of atomic variables.
  • Higher efficiency: Except for highly competitive situations, using atomic classes is usually more efficient than using synchronized locks because atomic classes utilize CAS operations at the underlying level and do not block threads.

6 Overview of atomic classes #

Now let’s take a look at the atomic classes and their categories. There are a total of 6 categories of atomic classes, which we will introduce one by one:

Category Specific classes
Atomic* basic type atomic classes AtomicInteger, AtomicLong, AtomicBoolean
Atomic*Array array type atomic classes AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
Atomic*Reference reference type atomic classes AtomicReference, AtomicStampedReference, AtomicMarkableReference
Atomic*FieldUpdater upgrade type atomic classes AtomicIntegerfieldupdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
Adder accumulators LongAdder, DoubleAdder
Accumulator accumulators LongAccumulator, DoubleAccumulator

Atomic\ basic type atomic classes #

Let’s first take a look at the first category, Atomic*. We call it the basic type atomic classes, which includes three types: AtomicInteger, AtomicLong, and AtomicBoolean.

Let’s introduce the most typical AtomicInteger. For this type, it is a wrapper for the int type and provides atomic access and updates. In other words, if we need an integer variable that will be used in a concurrent scenario, we can use AtomicInteger directly instead of the basic int type or the wrapper type Integer. This way, we automatically have atomic capabilities and it is very convenient to use.

Common methods of the AtomicInteger class #

The AtomicInteger class has several commonly used methods:

  • public final int get() // Gets the current value

Since it is a Java class and no longer a basic type, we need some methods to retrieve the value, such as the get method, which returns the current value.

  • public final int getAndSet(int newValue) // Gets the current value and sets a new value

The next few methods are related to its usual operations:

  • public final int getAndIncrement() // Gets the current value and increments it
  • public final int getAndDecrement() // Gets the current value and decrements it
  • public final int getAndAdd(int delta) // Gets the current value and adds the expected value

The delta parameter represents the value by which we want to change the current atomic class value. It can be a positive or negative number, where positive represents an addition and negative represents a subtraction. The getAndIncrement and getAndDecrement methods modify the value by default with +1 or -1. If this doesn’t meet our requirements, we can use the getAndAdd method to directly add or subtract the desired value in one go.

  • boolean compareAndSet(int expect, int update) // Atomically sets the value to the given updated value if the current value equals the expected value

This method is also an important implementation of CAS.

Array type atomic classes #

Next, let’s take a look at the second category, AtomicArray array type atomic classes. The elements in the array can all guarantee atomicity. For example, AtomicIntegerArray aggregates AtomicInteger together to form an array. This means that if we want to use an array where each element has atomicity, we can use AtomicArray.

There are three types in this category:

  • AtomicIntegerArray: Integer array atomic class
  • AtomicLongArray: Long array atomic class
  • AtomicReferenceArray: Reference type array atomic class.

Atomic Reference Type Atomic Reference #

Next, let’s introduce the third type of atomic reference called AtomicReference. The purpose of the AtomicReference class is not fundamentally different from AtomicInteger. AtomicInteger ensures the atomicity of an integer, while AtomicReference ensures the atomicity of an object. As a result, AtomicReference is more powerful than AtomicInteger because an object can contain many properties.

In this category, in addition to AtomicReference, we also have:

  • AtomicStampedReference: It is an upgrade to AtomicReference that adds a timestamp to solve the CAS ABA problem.
  • AtomicMarkableReference: Similar to AtomicReference, it also has a bound boolean value that can be used to indicate scenarios such as the object being deleted.

Atomic Field Updater #

The fourth category we will introduce is Atomic Field Updater, which consists of three types:

  • AtomicIntegerFieldUpdater: Updater for atomic updates to integers.
  • AtomicLongFieldUpdater: Updater for atomic updates to long integers.
  • AtomicReferenceFieldUpdater: Updater for atomic updates to references.

If we already have a variable, such as an integer int, it does not actually have atomicity. However, since the variable has already been defined, is there a way to give it atomicity? There is a way, and that is to use Atomic*FieldUpdater. If the variable is an integer, we can use AtomicIntegerFieldUpdater to upgrade the already declared variable, giving it the ability to perform CAS operations.

The non-exclusive synchronization method here is to perform CAS operations on the already declared variable to achieve synchronization. You might wonder, why not declare the variable as AtomicInteger from the beginning if we want it to have atomicity? This would also eliminate the need for upgrading, so was the initial design not reasonable? There are several situations to consider:

The first situation is due to historical reasons. If, for historical reasons, the variable has already been declared and widely used, modifying it would be costly. In this case, we can use the upgraded atomic class.

Another use case is when we do not need atomicity in most cases, only in a few cases such as once or twice a day during scheduled operations. In this case, it is not necessary to declare the original variable as an atomic type because AtomicInteger consumes more resources than a regular variable. So if we have thousands of instances of atomic classes, they would occupy much more memory than thousands of regular types. In this situation, we can use AtomicIntegerFieldUpdater to perform a reasonable upgrade and save memory.

Let’s take a look at some code:

public class AtomicIntegerFieldUpdaterDemo implements Runnable{

   static Score math;

   static Score computer;

   public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater

           .newUpdater(Score.class, "score");

   @Override

   public void run() {

       for (int i = 0; i < 1000; i++) {

           computer.score++;

           scoreUpdater.getAndIncrement(math);

       }

   }

   public static class Score {

       volatile int score;

   }

   public static void main(String[] args) throws InterruptedException {

       math =new Score();

       computer =new Score();

       AtomicIntegerFieldUpdaterDemo2 r = new AtomicIntegerFieldUpdaterDemo2();

       Thread t1 = new Thread(r);

       Thread t2 = new Thread(r);
       private static final long valueOffset;
    
       static {
    
           try {
    
               valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    
           } catch (Exception ex) {
    
               throw new Error(ex);
    
           }
    
       }
    
     
    ```
    
    首先,在 AtomicInteger 类的静态代码块中,通过调用 `Unsafe.getUnsafe()` 方法获取 Unsafe 类的实例,然后通过 `unsafe.objectFieldOffset()` 方法获取了 `AtomicInteger` 类中 `value` 字段的内存偏移量。
    
    接下来,我们再来看 `getAndAddInt` 方法的实现:
    
    ```
    
       final int getAndAddInt(Object obj, long offset, int delta) {
    
           int v;
    
           do {
    
               v = unsafe.getIntVolatile(obj, offset);
    
           } while (!unsafe.compareAndSwapInt(obj, offset, v, v + delta));
    
           return v;
    
       }
    
     
    ```
    
    在 `getAndAddInt` 方法中,使用了一个 `do-while` 循环,首先调用 `unsafe.getIntVolatile` 方法获取当前内存中的字段值,然后使用 `unsafe.compareAndSwapInt` 方法进行 CAS 操作,将内存中的字段值与预期值进行比较,如果相等,则将字段值更新为新值(原始值增加 `delta`)。
    
    如果 CAS 操作成功,返回自增前的字段值;否则,重复执行获取当前值和 CAS 操作,直到 CAS 操作成功为止。
    
    通过 `getAndAdd` 方法和 `getAndAddInt` 方法的实现,AtomicInteger 类能够实现并发下的累加操作,保证了原子性。
    
    这就是利用 CAS(Compare and Swap)操作实现原子操作的基本流程。
```java
private static final long valueOffset;

static {

    try {

        valueOffset = unsafe.objectFieldOffset

            (AtomicInteger.class.getDeclaredField("value"));

    } catch (Exception ex) { throw new Error(ex); }

}

private volatile int value;

public final int get() {return value;}

...

}

It can be seen from the data definition section that Unsafe instance is obtained, and valueOffset is defined. Moving down, we see the static block of code, which will be executed when the class is loaded. When executed, we call the objectFieldOffset method of Unsafe to obtain the offset of the value field of the current atomic class and assign it to the valueOffset variable. In this way, we obtain the offset of the value field, which represents the offset address in memory. Because Unsafe retrieves the original value of data based on the memory offset address, we can implement CAS through Unsafe.

The value field is marked with volatile, which is the variable storing the value of the atomic class. Because it is marked with volatile, we can ensure that the value seen by multiple threads is the same, ensuring visibility.

Next, let’s continue to analyze the implementation of the getAndAddInt method of Unsafe:

public final int getAndAddInt(Object var1, long var2, int var4) {

   int var5;

   do {

       var5 = this.getIntVolatile(var1, var2);

   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

   return var5;

}

First, let’s look at the structure. It is a do-while loop, so it is an infinite loop that only exits when the exit condition of the loop is met.

Now let’s take a look at the line of code after do, which is var5 = this.getIntVolatile(var1, var2). This is a native method that retrieves the value at the offset var2 in var1.

What are the parameters passed in? The two parameters passed in are the current atomic class and the offset we obtained earlier. In this way, we can get the value of the offset in the current memory and save it in var5. At this time, var5 actually represents the value of the atomic class at the current moment.

Now let’s look at the exit condition of while, which is the compareAndSwapInt method. It takes four parameters: var1, var2, var5, and var5 + var4. For easier understanding, we give them new names: object, offset, expectedValue, and newValue, with the following specific meanings:

  • The first parameter object is the object to be operated on, in this case, it is the atomicInteger object itself.
  • The second parameter is offset, which is the offset. With it, we can obtain the value of value.
  • The third parameter expectedValue represents the “expected value” and is set to the previously obtained var5.
  • The last parameter newValue is the value we want to modify, which is equal to the previously obtained value var5 plus var4. var4 is the delta we passed in earlier, and delta is the value we want the atomic class to change, such as +1 or -1.

Therefore, the compareAndSwapInt method checks if the value in the atomic class value field is equal to the previously obtained var5. If they are equal, the calculated var5 + var4 is updated. In this way, this line of code implements the CAS process.

Once the CAS operation is successful, it will exit the while loop, but it may also fail. If the operation fails, it means that after obtaining var5 and before the CAS operation, the value of value has changed, indicating that another thread has modified this variable.

In this case, the code block inside the loop will be executed again to obtain the latest value of var5, which is the latest value of the atomic variable, and then try to update it using CAS until the update is successful. So this is an infinite loop.

In summary, the Unsafe getAndAddInt method is implemented through a loop + CAS. In this process, it tries to update the value using the compareAndSwapInt method. If the update fails, it tries to obtain the value again and tries to update it again until the update is successful.

Summary #

In this lesson, we first introduced the role of atomic classes, and then introduced the 6 types of atomic classes: Atomic* basic type atomic classes, AtomicArray array type atomic classes, AtomicReference reference type atomic classes, Atomic*FieldUpdater upgraded type atomic classes, Adder adder, and Accumulator accumulator.

Then we introduced them one by one, understanding their basic functions and usages. Next, using AtomicInteger as an example, we analyzed how atomic operations are implemented using CAS in Java.

Starting with the getAndAdd method, we gradually delve into the Unsafe getAndAddInt method. Therefore, after analyzing the source code, we clearly see that the implementation principle is to use spinning to constantly attempt until success.