Back to Blog
Java

Java AtomicInteger for Thread-Safe Counters

java atomicinteger: Learn how to use Java AtomicInteger for thread-safe counters and atomic operations without locks, including core methods, performance, and pitfalls.

concurrencyatomic-variablesthread-safetyjava-concurrencylock-free
Illustration of Java AtomicInteger providing atomic increments for thread-safe counters.

java atomicinteger requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When multiple threads update a shared integer, the result can be incorrect because read-modify-write operations are not atomic. Java's AtomicInteger solves this by providing a thread-safe integer that can be updated without explicit synchronization. It is part of the java.util.concurrent.atomic package and relies on low-level atomic instructions, making it a practical choice for counters, sequence generators, and other state that must be updated concurrently.

The Race Condition That AtomicInteger Solves

Consider a simple counter that is incremented by several threads:

public class UnsafeCounter { private int count; public void increment() { count++; } public int get() { return count; } }

The count++ operation is not atomic. It reads the current value, adds one, and writes the result back. If two threads execute this at the same time, one increment can be lost. The classic fix is to synchronize the method or use a lock, but that adds contention and can reduce throughput. AtomicInteger avoids this by performing the entire operation as a single atomic unit.

Creating and Updating an AtomicInteger

Creating an AtomicInteger is straightforward:

AtomicInteger counter = new AtomicInteger(0);

You can also use the no-argument constructor, which initializes the value to zero. To update the value, the most common method is incrementAndGet():

int newValue = counter.incrementAndGet();

This increments the current value and returns the new value. The counterpart getAndIncrement() returns the old value before incrementing. Both are atomic and equivalent to ++count and count++ respectively, but without the race condition.

The Core Methods and Their Semantics

AtomicInteger provides a rich set of atomic operations. The most frequently used are:

  • get() and set(int) for simple reads and writes.
  • addAndGet(int delta) and getAndAdd(int delta) for adding a fixed amount.
  • compareAndSet(int expect, int update) for conditional updates.
  • updateAndGet(IntUnaryOperator) and getAndUpdate(IntUnaryOperator) for functional updates.

compareAndSet is particularly important. It checks whether the current value equals expect and, if so, sets it to update. The operation returns true if the update was performed, false otherwise. This is the foundation for lock-free algorithms. For example, you can implement a non-blocking retry loop:

public void incrementIfZero(AtomicInteger counter) { while (true) { int current = counter.get(); if (current != 0) { return; } if (counter.compareAndSet(0, 1)) { return; } } }

If another thread changes the value between the get() and compareAndSet, the loop retries. This pattern is common in lock-free data structures.

How AtomicInteger Achieves Thread Safety

AtomicInteger does not use locks. Instead, it relies on the sun.misc.Unsafe class (or its internal replacement) to issue compare-and-swap (CAS) instructions at the hardware level. CAS is a CPU instruction that atomically compares a memory location to a given value and, if they match, updates it to a new value. The JVM ensures that this instruction is atomic even on multi-core systems.

Because there is no lock, there is no context switching or thread blocking. Under low contention, CAS operations are much faster than synchronized blocks. Under high contention, however, repeated CAS failures can cause high CPU usage because threads spin in a loop. This is a trade-off: AtomicInteger is lock-free but not always wait-free. In practice, for counters with moderate contention, it performs well.

When to Use AtomicInteger Instead of Synchronized or Volatile

volatile ensures visibility but not atomicity. If you only need to read and write a single integer without compound operations, volatile is sufficient. But for increments, decrements, or any read-modify-write sequence, volatile is not enough. synchronized provides both atomicity and visibility, but it can introduce contention and thread suspension. AtomicInteger sits in between: it provides atomicity and visibility without locking.

ApproachAtomicityVisibilityLockingTypical Use Case
volatileNoYesNoFlags, simple reads/writes
synchronizedYesYesYesComplex critical sections
AtomicIntegerYesYesNoCounters, accumulators, sequence numbers

Use AtomicInteger when the operation is a simple update on a single integer and you want to avoid lock overhead. If you need to coordinate multiple variables or execute a block of code atomically, synchronized is still the right tool. For read-heavy workloads where the value rarely changes, volatile may be enough.

Common Mistakes and Limitations

One common mistake is assuming that AtomicInteger makes all compound operations safe. For example, incrementAndGet() is atomic, but a sequence like if (counter.get() > 0) { counter.decrementAndGet(); } is not. Between the get() and decrementAndGet(), another thread can change the value. You need compareAndSet or a loop to handle such cases.

Another limitation is that AtomicInteger only works for a single integer. If you need to atomically update two values together, such as a pair of coordinates, you must use a different mechanism, like AtomicReference to an immutable object or a lock. Also, AtomicInteger does not provide a compareAndSet that works with a maximum or minimum value; you have to implement that with a loop.

Finally, AtomicInteger is not a replacement for LongAdder when you have extremely high write contention. LongAdder uses striped counters to reduce CAS contention, at the cost of slightly higher memory usage. For most applications, AtomicInteger is sufficient, but if profiling shows excessive CAS retries, consider LongAdder.

AtomicInteger in Real-World Concurrency Patterns

A common pattern is using AtomicInteger as a unique ID generator. The following code safely hands out increasing IDs:

public class IdGenerator { private final AtomicInteger nextId = new AtomicInteger(0); public int next() { n return nextId.incrementAndGet(); } }

This works without synchronization and is is safe across threads. Another pattern is using AtomicInteger as a bounded counter, for example, to limit the number of concurrent tasks. You can use incrementAndGet() before starting a task and decrementAndGet() after finishing, with compareAndSet to enforce the bound.

In a producer-consumer scenario, AtomicInteger can track the number of items in a buffer without locks. However, you must be careful about memory visibility: AtomicInteger provides it, but but you still need to proper synchronization for the actual data structure. The atomic integer only ensures that the count itself is consistent.

When designing lock-free algorithms, AtomicInteger is often the building block. The compareAndSet method enables optimistic concurrency: you read a value, compute a new one, and attempt to update, retrying if the value changed. This pattern is the core of many non-blocking data structures, such as concurrent stacks and queues. Understanding AtomicInteger is therefore essential for anyone working on high-performance concurrent Java code.

java atomicinteger: Practical Usage and Code Examples | RYUSLOG DEV