In multi-threaded Java programming, one of the most common challenges is safely managing shared data. When multiple threads read and write to a simple variable like an int or a long, you can encounter race conditions, leading to inconsistent and incorrect results. While the synchronized keyword provides a solution, it can be heavy-handed, leading to performance bottlenecks due to thread blocking.
This is where the java.util.concurrent.atomic package comes to the rescue. It provides a set of classes that offer thread-safe operations on single variables without the need for locking. In this article, we'll focus on two of its most fundamental classes: AtomicInteger and AtomicLong.
The Problem: The Volatile Pitfall
A common misconception is that marking a variable with the volatile keyword is sufficient for all thread-safe operations. volatile guarantees visibility (a write by one thread is immediately visible to others) and ordering, but it does not guarantee atomicity for compound actions.
Consider this non-thread-safe counter:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // This is a read-modify-write operation
}
public int getCount() {
return count;
}
}
The count++ operation may look like a single action, but it's actually a three-step process:
- Read the current value of
count. - Increment the value.
- Write the new value back.
If two threads read the value (say, 10) at the same time, both increment it to 11, and both write 11 back, the final result is 11 instead of the correct 12.
The Solution: AtomicInteger and AtomicLong
AtomicInteger and AtomicLong are designed to solve this exact problem. They wrap an int or long value and provide methods to perform common operations on it atomically.
Key Features:
- Lock-Free: They typically use low-level CPU instructions (Compare-And-Swap, or CAS) instead of traditional locks, making them very efficient.
- Volatile Semantics: All operations have the memory effects of reading and writing
volatilevariables. - Atomic Operations: Methods like
incrementAndGet()are executed as a single, uninterruptible unit.
Core Methods and Usage
Both classes have a very similar API. Here are the most commonly used methods for AtomicInteger (the same methods exist for AtomicLong).
1. Creation
AtomicInteger atomicInt = new AtomicInteger(); // initial value 0 AtomicInteger atomicInt = new AtomicInteger(10); // initial value 10 AtomicLong atomicLong = new AtomicLong(1000L);
2. Basic Get and Set
atomicInt.set(15); // Sets the value to 15 int value = atomicInt.get(); // Retrieves the current value
3. Atomic Increment and Decrement
These are the most famous methods, perfect for counters.
// Pre-increment: similar to ++i int newValue = atomicInt.incrementAndGet(); // Post-increment: similar to i++ int oldValue = atomicInt.getAndIncrement(); // Pre-decrement: similar to --i int newValue = atomicInt.decrementAndGet(); // Post-decrement: similar to i-- int oldValue = atomicInt.getAndDecrement();
4. Atomic Compare-and-Set (CAS)
This is the fundamental operation behind most atomic classes. It only sets the new value if the current value matches the expected one.
boolean success = atomicInt.compareAndSet(expect, update);
This is incredibly useful for implementing non-blocking algorithms.
5. Atomic Addition
// Add and get the new value int newValue = atomicInt.addAndGet(5); // Get the old value and then add int oldValue = atomicInt.getAndAdd(5);
Practical Example: A High-Performance Counter
Let's see how we can create a thread-safe counter that can be accessed by 1,000 threads simultaneously.
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Thread-safe and efficient!
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
final SafeCounter counter = new SafeCounter();
// Create 1000 threads that each increment the counter
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// Wait for all threads to finish
for (Thread t : threads) {
t.join();
}
// The result will always be 1,000,000
System.out.println("Final count: " + counter.getCount());
}
}
Output:
Final count: 1000000
This result is guaranteed to be correct every time, without using the synchronized keyword.
When to Use AtomicInteger/AtomicLong
- Simple State Variables: Ideal for counters, sequence generators, or flags where a single variable needs to be updated atomically.
- Performance-Critical Sections: When you need thread-safety but want to avoid the overhead of
synchronizedblocks. - Building Blocks: They are often used as the foundation for more complex, non-blocking data structures.
When to Look Elsewhere
- Multiple Variables: If you need to update multiple related variables atomically as a single unit, you should use locks (
synchronized,ReentrantLock) or other thread-safe classes. - Complex Conditions: For operations that depend on complex conditions involving multiple variables, the CAS approach can become cumbersome.
Conclusion
AtomicInteger and AtomicLong are indispensable tools in the modern Java developer's toolkit for concurrent programming. They provide a simple, elegant, and high-performance solution for managing single-threaded variables in a multi-threaded environment. By understanding and using these classes, you can write concurrent code that is not only correct but also highly efficient.