CodeGym /Courses /JAVA 25 SELF /AtomicInteger, AtomicReference: atomic operations

AtomicInteger, AtomicReference: atomic operations

JAVA 25 SELF
Level 53, Lesson 3
Available

1. Why i++ doesn’t work in multithreaded code?

Let’s start with a classic task: we have a counter variable, for example, the number of processed requests or downloaded files. We want several threads to increment this counter. What could go wrong if we just write i++?

Example: a data race on increment

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // Not atomic!
    }
}

Imagine two threads call increment() at the same time. Both threads read the old value, both increase it by 1, and both write... the same new value! As a result, one increment is “lost.” If you repeat this many times, the final value will be less than expected.

Why does this happen?
The i++ operation actually consists of three steps:

  1. Read the variable’s value (for example, 5).
  2. Increase that value by 1.
  3. Write the new value back to memory.

And in a multithreaded environment, another thread may change the variable between these steps. The result is a “data race” (race condition).

What are atomic operations?

An atomic operation is an action that either completes entirely or doesn’t happen at all, and no other thread can interleave in the middle of this operation.

Java provides a set of classes that offer such operations for primitives and references. They are located in the java.util.concurrent.atomic package. The most popular are:

  • AtomicInteger — an atomic integer type.
  • AtomicLong — an atomic long.
  • AtomicBoolean — an atomic boolean.
  • AtomicReference<T> — an atomic reference to an object of any type.

2. AtomicInteger: a thread-safe counter

Declaration and basic usage

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Atomic increment
    }

    public int get() {
        return count.get();
    }
}

Here, incrementAndGet() performs “increment and return the new value” as a single indivisible operation. Even if 100 threads call this method simultaneously, no increment will be lost.

Useful methods:

Method Description
get()
Get the current value
set(int value)
Set the value
incrementAndGet()
Increment by 1 and return the new value
getAndIncrement()
Return the current value and increment by 1
addAndGet(int delta)
Add delta and return the new value
compareAndSet(expect, update)
If the current value equals expect, set it to update (CAS)

Example: a multithreaded counter

Suppose we have a class that counts the number of processed messages in a chat.

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("Total messages: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

Under the hood: how does AtomicInteger work?

Internally, AtomicInteger uses a special CPU instruction — CAS (Compare-And-Swap). This is an atomic operation that compares the current value of a variable with the expected one, and if they match, writes the new value. If another thread manages to change the variable, the operation doesn’t complete, and the attempt is retried.

How it works:

1. Read the current value (for example, 5)
2. Compare with the expected value (5)
3. If it matches — write the new value (6)
4. If it doesn't — retry

All of this happens very quickly and without locks (lock‑free). That’s why atomic classes are often faster than synchronized, especially with a large number of threads.

3. AtomicReference: an atomic reference to an object

AtomicReference<T> is a general-purpose atomic container for any object. It lets you safely change a reference from different threads.

Example: thread-safe reference update

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

Using compareAndSet

The most interesting operation is compareAndSet(expected, newValue). It allows you to update the value only if it hasn’t changed since the last read.

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("Update succeeded!");
    } else {
        System.out.println("Someone has already changed the value, please try again.");
    }
}

This is the basis of non-blocking algorithms: from queues and stacks to caches, where avoiding unnecessary locks is important.

4. Usage examples in an application

Example 1: a multithreaded message counter

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... message processing ...
        int count = messageCount.incrementAndGet();
        System.out.println("New message: " + message + ". Total messages: " + count);
    }
}

Example 2: safely updating the reference to the last message

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... processing ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

If you need to update the reference only if the last message hasn’t changed (to avoid “lost” updates under concurrent modifications), use compareAndSet.

5. Limitations and pitfalls

When atomic classes are not a silver bullet

Atomic variables are great for simple operations: increment, setting a value, check-and-swap. But if you need to update several variables at once, atomicity is no longer guaranteed. For example, if you have two counters and want to increment both as a single operation — you need synchronized or another synchronization mechanism.

Incorrect usage example

// NOT atomic!
if (ref.get() == null) {
    ref.set("Hello");
}

Between get() and set(...), another thread may change the value, and the condition will no longer hold. For such cases, use compareAndSet.

Atomic classes ≠ thread-safe objects

If the object referenced by AtomicReference isn’t itself thread-safe, replacing the reference will be atomic, but modifying the object’s fields will not. For example, if you store a regular ArrayList in AtomicReference<List<String>>, the list itself doesn’t become thread‑safe.

6. Advanced atomic classes

The java.util.concurrent.atomic package also offers other useful classes:

  • AtomicLong, AtomicBoolean — for long and boolean.
  • AtomicIntegerArray, AtomicReferenceArray — atomic operations on arrays.
  • LongAdder, LongAccumulator — for highly contended counters.

LongAdder and LongAccumulator

If you have a lot of threads and a regular AtomicInteger becomes a bottleneck (all threads contend for a single variable), use LongAdder. It splits the counter into several internal cells and sums them when the value is requested, which provides a speedup under high contention.

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. Common mistakes when working with atomic variables

Mistake #1: Expecting complex operations to be atomic.
If you need to perform several actions on a value, atomic classes won’t save you — another thread may change the data between steps. For compound operations, use compareAndSet or synchronization.

Mistake #2: Ignoring the thread safety of nested objects.
If an ordinary object is stored in an AtomicReference, its methods and fields don’t become thread-safe. Only the reference replacement is atomic.

Mistake #3: Using atomic classes unnecessarily.
In single-threaded code, atomic types are superfluous and slightly slower than regular variables due to additional checks.

Mistake #4: Premature optimization.
Sometimes it’s simpler and more reliable to use synchronized, especially when the logic is complex and touches several variables at once. Building lock‑free solutions isn’t always justified.

Mistake #5: Forgetting about the ABA problem.
A rare but important case: the value changes from A to B and back to A — compareAndSet “thinks” nothing has changed. For such scenarios, use special classes like AtomicStampedReference (or AtomicMarkableReference).

1
Task
JAVA 25 SELF, level 53, lesson 3
Locked
Counting Visitors at a Mega-Event: Thread-Safe Counter 🏟️
Counting Visitors at a Mega-Event: Thread-Safe Counter 🏟️
1
Task
JAVA 25 SELF, level 53, lesson 3
Locked
Updating the Current Guild Leader: Safe Object Update with AtomicReference 👑
Updating the Current Guild Leader: Safe Object Update with AtomicReference 👑
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION