Understanding Java AtomicInteger with an Example
When multiple threads access and modify the same variable, unexpected results can occur due to race conditions. A simple int variable is not thread-safe when it is updated concurrently. Java provides the AtomicInteger class in the java.util.concurrent.atomic package to safely perform atomic operations on integer values without requiring explicit synchronization in many common scenarios. This article demonstrates why a normal int can produce incorrect results in a multithreaded application and how AtomicInteger solves the problem.
1. Understanding the Problem: Why a Normal int Breaks
Consider a shared counter: private int counter = 0;. You might expect the following operation to safely increment the counter: counter++;. However, counter++ is not a single atomic operation. Conceptually, it involves multiple steps:
- Read the current value of
counter. - Increment the value.
- Write the new value back.
When multiple threads perform these steps simultaneously, two threads can read the same value before either writes its updated value. For example:
Thread 1 reads counter = 10 Thread 2 reads counter = 10 Thread 1 increments and writes 11 Thread 2 increments and writes 11
Although two increments were performed, the counter increased from 10 to only 11. One update was lost.
1.1 Understanding AtomicInteger
AtomicInteger provides thread-safe atomic operations for working with integer values in concurrent applications. Common methods include incrementAndGet(), which increments the value and returns the updated value; getAndIncrement(), which returns the current value before incrementing it; addAndGet(), which atomically adds a specified value and returns the result; and compareAndSet(), which updates the value only if it currently matches an expected value. These operations allow multiple threads to safely modify a shared integer without losing updates due to race conditions.
1.1.1 Common Mistakes
- Do not assume that using
AtomicIntegerautomatically makes all operations in a class thread-safe. Only the operations performed atomically on theAtomicIntegeritself are protected. - If an operation involves multiple shared variables or multiple steps that must happen together, you may need synchronization or another concurrency mechanism.
- Avoid using
get()followed by a separateset()when the read and update need to happen atomically. Another thread can change the value between these two operations. - Use methods such as
compareAndSet()orupdateAndGet()when the condition, read, and update need to be handled as a single atomic operation.
1.2 When to Use int and When to Use AtomicInteger
Use int when | Use AtomicInteger when |
|---|---|
| The value is accessed by a single thread. | Multiple threads update the same value. |
| The value is local to a method or object without concurrent access. | You need atomic increment, decrement, or update operations. |
| Thread safety is already handled using another synchronization mechanism. | You need operations such as compareAndSet() or updateAndGet(). |
In simple single-threaded code, a normal int is usually the right choice. When multiple threads need to update a shared integer independently and atomically, AtomicInteger is often a better fit.
2. Java Code Example
The following example uses multiple threads to increment both a normal int counter and an AtomicInteger counter.
// AtomicIntegerExample.java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static int normalCounter = 0;
private static final AtomicInteger atomicCounter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
int numberOfThreads = 10;
int incrementsPerThread = 10000;
Thread[] threads = new Thread[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
// Not thread-safe
normalCounter++;
// Thread-safe atomic operation
atomicCounter.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
int expectedValue = numberOfThreads * incrementsPerThread;
System.out.println("Expected value: " + expectedValue);
System.out.println("Normal int counter: " + normalCounter);
System.out.println("AtomicInteger counter: " + atomicCounter.get());
// compareAndSet: change the value only if it still matches
boolean updated = atomicCounter.compareAndSet(100000, 200000);
System.out.println("compareAndSet successful: " + updated);
System.out.println("Value after compareAndSet: " + atomicCounter.get());
// updateAndGet: apply your own rule atomically
int updatedValue = atomicCounter.updateAndGet(value ->
value > 150000 ? value + 1000 : value);
System.out.println("Value after updateAndGet: " + updatedValue);
}
}
2.1 Code Explanation
The example first imports AtomicInteger and creates two shared counters: a normal int named normalCounter and an AtomicInteger named atomicCounter. The program then creates 10 threads, with each thread incrementing both counters 10,000 times. While normalCounter++ is not thread-safe and can result in lost updates when multiple threads access it concurrently, atomicCounter.incrementAndGet() performs each increment atomically. After starting all threads with start(), the program uses join() to wait for every thread to finish before reading the final counter values. It then calculates the expected value as 10 × 10,000 = 100,000 and prints the results. The normal int counter may produce a value lower than 100,000 because of race conditions, while the AtomicInteger counter should consistently reach 100,000. The example then uses compareAndSet() to change the atomic counter from 100,000 to 200,000 only if its current value still matches 100,000. Finally, updateAndGet() applies a custom rule that adds 1,000 when the current value is greater than 150,000, demonstrating how AtomicInteger can safely perform conditional and custom atomic updates.
2.2 Code Output
When the program runs, the expected value is 100000 because 10 threads each perform 10,000 increments. The AtomicInteger counter reaches exactly 100000 because incrementAndGet() performs each increment atomically. The normal int counter may produce a lower value because concurrent threads can overwrite each other’s updates.
Expected value: 100000 Normal int counter: 87432 AtomicInteger counter: 100000 compareAndSet successful: true Value after compareAndSet: 200000 Value after updateAndGet: 201000
The exact value of the normal int counter can vary between executions depending on thread scheduling. After the concurrent operations complete, compareAndSet() successfully changes the value from 100000 to 200000 because the current value matches the expected value. The updateAndGet() operation then applies the custom rule and adds 1000 because the current value is greater than 150000, resulting in 201000. This demonstrates how AtomicInteger supports not only thread-safe increments but also conditional and custom atomic updates.
3. Conclusion
A normal int is suitable when only one thread modifies the value or when proper synchronization is already in place. However, using counter++ directly in concurrent code can lead to race conditions and lost updates. AtomicInteger provides a simple way to perform thread-safe operations on integer values. For counters and similar concurrent operations, methods such as incrementAndGet() allow multiple threads to safely update shared state without manually managing synchronization.

