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:
- Read the variable’s value (for example, 5).
- Increase that value by 1.
- 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 the current value |
|
Set the value |
|
Increment by 1 and return the new value |
|
Return the current value and increment by 1 |
|
Add delta and return the new value |
|
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).
GO TO FULL VERSION