Problem Background

In a multi-threaded environment, the most common problem we encounter is synchronizing the values of variables. Since variables need to be shared across multiple threads, we must need to employ some synchronization mechanism to control them.

From the previous article we know that the Lock mechanism can be used, and of course, the Atomic class we are talking about today.

Here we will introduce each of the two ways.

Lock

In the previous article, we also talked about synchronization, so let’s review it again. If a counter is defined as follows.

1
2
3
4
5
6
7
8
9
public class Counter {

    int counter;

    public void increment() {
        counter++;
    }

}

If it is in a single threaded environment, the above code has no problem. But in a multi-threaded environment, counter++ will give a different result.

Because although counter++ looks like an atomic operation, it actually contains three operations: read the data, add one, and write back the data.

Our previous article also talks about how to solve this problem.

1
2
3
4
5
6
7
8
public class LockCounter {

    private volatile int counter;

    public synchronized void increment() {
        counter++;
    }
}

By adding synchronized, it is guaranteed that only one thread will be reading and writing the counter variable at the same time.

By volatile, it is guaranteed that all data is directly manipulated in the main cache without using the thread cache.

This solves the problem, but performance may suffer because synchronized locks the entire LockCounter instance.

Using Atomic

By introducing low-level atomic semantic commands (such as compare-and-swap (CAS)), atomicity can be guaranteed while maintaining efficiency.

A standard CAS consists of three operations.

  1. the memory address M to be manipulated.
  2. the existing variable A.
  3. the new variable B to be stored.

CAS will first compare the values stored in A and M. If they are the same, the variable is replaced with B if no other thread has modified it. Otherwise, no operation is performed.

Using CAS can be done without blocking other threads, but we need to handle our own business logic in case of update failure.

Java provides many Atomic classes, the most common ones include AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference.

The main methods are.

  1. get() - Reads the value of a variable directly from main memory, similar to a volatile variable.
  2. set() - Writes the variable back to main memory. Similar to volatile variables.
  3. lazySet() - Delays writing back to main memory. A common scenario is to reset a reference to null.
  4. compareAndSet() - Performs a CAS operation, returns true on success, false on failure.
  5. weakCompareAndSet() - A weaker CAS operation, the difference being that it does not perform a happens-before operation and thus is not guaranteed to read the latest values of other variables.

Let’s see how it works.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class AtomicCounter {

    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}