A Java AtomicInteger is an int value that many threads can update safely without a lock. We use AtomicInteger for counters and sequence numbers that several threads change at the same time, for example the download count of a file server that handles each request on its own thread.
The following example lets four threads add 1 to two counters, a plain int with count++ and an AtomicInteger with incrementAndGet(). Each thread runs each line 10,000,000 times, and we repeat the run five times. The output also shows a volatile int counter for comparison.
static int plainDownloads = 0;
static final AtomicInteger downloads = new AtomicInteger();
// 4 threads run each line 10,000,000 times
plainDownloads++; // plainDownloads = 38231524 (wrong)
downloads.incrementAndGet(); // downloads.get() = 40000000
Round 1: int = 38231524, volatile int = 39876778, AtomicInteger = 40000000
Round 2: int = 37074328, volatile int = 37957833, AtomicInteger = 40000000
Round 3: int = 40000000, volatile int = 31298287, AtomicInteger = 40000000
Round 4: int = 37230525, volatile int = 36579277, AtomicInteger = 40000000
Round 5: int = 40000000, volatile int = 39831594, AtomicInteger = 40000000
Notice that the int counter lost up to 2,925,672 updates, and marking the field volatile did not help. The AtomicInteger counter reached 40,000,000 in every round.
Next, we see why count++ loses updates and go through every AtomicInteger method with its result. After that, we look at compare-and-swap, compare AtomicInteger with synchronized and volatile, and switch to LongAdder when many threads update the same counter.
1. What Is AtomicInteger?
AtomicInteger is a class in the java.util.concurrent.atomic package, available since Java 5. The class holds one int value and offers methods that read and change the value in one atomic step (that is, a step that no other thread can interrupt halfway).
The line count++ looks like one operation, but the CPU runs it as three steps, which read the value, add 1 and write the result back. When two threads read the same value, for example 41, both threads write 42, so one of the two updates is lost. The bug is called a race condition, because the result depends on which thread runs first.
The volatile keyword does not fix count++. A volatile field makes every write visible to other threads at once, but count++ still has three separate steps, which is why the volatile int column in the intro output is also wrong.
The method incrementAndGet() does all three steps as one atomic operation, so no other thread can read the old value in between. Code that gives the correct result when many threads run it at the same time is called thread-safe.
The typical use case is a counter that many threads increment, and AtomicInteger cannot replace Integer in general code. The class extends Number, but AtomicInteger does not override equals() or hashCode(), so we do not use it as a HashMap key or compare two counters with equals().
2. AtomicInteger Methods
Every AtomicInteger method reads or changes the value atomically. Most methods come in pairs that differ only in what they return. The getAndX() methods return the old value, and the xAndGet() methods return the new value.
| Method | What it does | Returns |
|---|---|---|
| get() | Reads the current value | the value |
| set(v) | Writes v | nothing |
| getAndSet(v) | Writes v | the old value |
| incrementAndGet() | Adds 1 | the new value, like ++i |
| getAndIncrement() | Adds 1 | the old value, like i++ |
| decrementAndGet(), getAndDecrement() | Subtracts 1 | the new or the old value |
| addAndGet(delta), getAndAdd(delta) | Adds delta, which can be negative | the new or the old value |
| compareAndSet(expected, v) | Writes v only if the value equals expected | true if it wrote |
| compareAndExchange(expected, v) | Same as compareAndSet() (Java 9+) | the value it found |
| updateAndGet(fn), getAndUpdate(fn) | Writes fn(value) (Java 8+) | the new or the old value |
| accumulateAndGet(x, fn), getAndAccumulate(x, fn) | Writes fn(value, x) (Java 8+) | the new or the old value |
2.1. Creating, Reading and Setting the Value
The no-argument constructor starts the counter at 0. The methods get() and set() read and write the value, and getAndSet() does both in one step.
AtomicInteger empty = new AtomicInteger(); // value = 0
AtomicInteger likes = new AtomicInteger(10); // value = 10
int current = likes.get(); // current = 10
likes.set(20); // value = 20
int old = likes.getAndSet(30); // old = 20, value = 30
2.2. incrementAndGet() vs getAndIncrement()
Both methods add 1 to the value, so the counter ends the same. The difference is the return value, which matters when we use the result. For example, a shop that numbers its orders with incrementAndGet() gets 1 for the first order, whereas getAndIncrement() gives 0.
likes.set(10);
int a = likes.incrementAndGet(); // a = 11, value = 11 (like ++i)
int b = likes.getAndIncrement(); // b = 11, value = 12 (like i++)
int c = likes.decrementAndGet(); // c = 11, value = 11 (like --i)
int d = likes.getAndDecrement(); // d = 11, value = 10 (like i--)
int e = likes.addAndGet(5); // e = 15, value = 15
int f = likes.getAndAdd(5); // f = 15, value = 20
int g = likes.addAndGet(-3); // g = 17, value = 17
There is no subtractAndGet() method, so we pass a negative number to addAndGet() instead.
2.3. compareAndSet() and compareAndExchange()
The method compareAndSet(expected, newValue) writes the new value only when the current value equals expected. It returns either true when it wrote the value, or false when it found another value, in which case the value stays unchanged.
likes.set(10);
boolean first = likes.compareAndSet(10, 50); // true, value = 50
boolean second = likes.compareAndSet(10, 70); // false, value = 50 (expected 10, found 50)
int witness = likes.compareAndExchange(50, 60); // witness = 50, value = 60
The method compareAndExchange(), added in Java 9, returns the value it found instead of a boolean. When the returned value equals expected, the write happened. When the returned value is different, the write did not happen, and we already have the current value for the next try.
2.4. updateAndGet() and accumulateAndGet()
For any change other than adding, we pass a function. The method updateAndGet() takes a function of the current value, and accumulateAndGet() takes a second argument and a function of two values.
likes.set(10);
int doubled = likes.updateAndGet(x -> x * 2); // doubled = 20, value = 20
int before = likes.getAndUpdate(x -> x * 2); // before = 20, value = 40
int sum = likes.accumulateAndGet(5, Integer::sum); // sum = 45, value = 45
int max = likes.accumulateAndGet(100, Math::max); // max = 100, value = 100 (keeps the maximum)
int prev = likes.getAndAccumulate(7, Math::min); // prev = 100, value = 7
The function passed to updateAndGet() or accumulateAndGet() must have no side effects, because AtomicInteger can call it more than once. When another thread changes the value during the update, the method calls the function again with the new value. So a function that writes a log line or sends a message can run twice for one update.
3. How Compare-and-Swap Works
AtomicInteger does not use a lock. The class relies on a CPU instruction called compare-and-swap (CAS), which compareAndSet() exposes in Java. CAS writes a new value only if the memory still holds the value we read earlier, and the CPU does the check and the write as one step.
To add 1 with CAS, a thread reads the value, computes the value plus 1, and calls compareAndSet(old, new). If another thread changed the value in the meantime, compareAndSet() returns false, so the thread reads the new value and tries again.

A failed CAS costs only one retry, whereas a thread that finds a lock taken may have to wait until the operating system wakes it up again. On x86 CPUs, the JVM runs incrementAndGet() and addAndGet() as a single atomic add instruction, so these two methods do not retry at all. The methods updateAndGet() and accumulateAndGet() always use the CAS loop.
3.1. Writing a CAS Loop
We write our own CAS loop when the new value depends on a condition. Say a file server allows 2,500,000 downloads per day, so the download counter must stop at that limit, and the thread checks the limit before every attempt.
static boolean tryDownload() {
while (true) {
int current = used.get(); // 1. read the current value
if (current >= DAILY_LIMIT) {
return false; // limit reached
}
int next = current + 1; // 2. compute the new value
if (used.compareAndSet(current, next)) { // 3. write only if nobody changed it
return true;
}
// another thread changed the value first, so read it again
}
}
// the same rule with getAndUpdate()
int before = used.getAndUpdate(x -> x < DAILY_LIMIT ? x + 1 : x);
boolean accepted = before < DAILY_LIMIT;
Four threads sent 1,000,000 download requests each. Both versions accepted 2,500,000 requests, and the loop counted how many compareAndSet() calls failed and retried.
CAS loop: requests = 4000000, accepted = 2500000, used = 2500000, failed CAS retries = 508514
updateAndGet: requests = 4000000, accepted = 2500000, used = 2500000
The number of retries changes on every run, from about 2,000 to 550,000 in our runs, but the accepted count is always 2,500,000. A plain if (count < limit) count++ with an int would accept more requests than the limit, because two threads can both pass the check before either one writes.
4. AtomicInteger vs synchronized vs volatile
The keyword volatile, the class AtomicInteger and the keyword synchronized all deal with data that threads share, but each one solves a different part of the problem. A volatile field gives visibility only, and AtomicInteger gives atomic updates on one value. A synchronized block gives mutual exclusion (only one thread at a time) for a whole block of code.
| volatile int | AtomicInteger | synchronized block | |
|---|---|---|---|
| Other threads see the latest value | yes | yes | yes, when they also synchronize |
| count++ is safe | no | yes, incrementAndGet() | yes |
| Check-then-act on one value | no | yes, compareAndSet() or updateAndGet() | yes |
| Update two or more fields together | no | no | yes |
| Threads wait for each other | no | no, a failed CAS retries | yes, waiting threads are blocked |
| Typical use | a flag such as running | counters, sequence numbers, statistics | invariants across several fields |
We use AtomicInteger when the shared state is one number, and synchronized or a Lock when two or more values must change together. For example, an online shop keeps stock and reserved in two separate AtomicInteger fields. Each field is safe on its own, but another thread can still see the moment when stock has changed and reserved has not.
A volatile field is the right choice when only one thread writes and the others only read, for example a boolean flag that tells a worker thread to stop.
5. LongAdder for High Contention
When many threads call incrementAndGet() on the same AtomicInteger, all of them update one memory location. The situation is called high contention, and the updates slow down because only one CPU core can change the value at a time.
LongAdder, added in Java 8, spreads the updates over several internal counters called cells. When threads collide, they move to different cells, so they rarely compete. The method sum() adds up all the cells to give the total.

LongAdder fits counters that many threads update often and that we read rarely, such as request counts or per-key statistics in a ConcurrentHashMap.
LongAdder downloads = new LongAdder();
Map<String, LongAdder> downloadsPerFile = new ConcurrentHashMap<>();
// 4 threads, 1,000,000 downloads each
downloads.increment();
downloadsPerFile.computeIfAbsent(file, f -> new LongAdder()).increment();
long total = downloads.sum(); // total = 4000000
long guide = downloadsPerFile.get("guide.pdf").sum(); // guide = 1000000
long reset = downloads.sumThenReset(); // reset = 4000000, and the adder is back to 0
Under low contention, LongAdder and AtomicLong behave about the same. Under high contention, LongAdder has a much higher throughput, at the cost of more memory.
In exchange, LongAdder gives up two things that AtomicInteger has.
- The method sum() is not an atomic snapshot. When threads update the adder while sum() runs, some of those updates may be missing from the result.
- LongAdder has no compareAndSet(), incrementAndGet() or updateAndGet(), so we cannot use it for sequence numbers or for a limit check like the one in section 3.1.
5.1. Measured Timings on a 2-CPU Machine
We timed 20,000,000 increments shared by 1, 2, 4 and 8 threads, using a plain timing loop (not JMH) with the median of 7 runs. The machine had only 2 CPUs and other jobs running, so these numbers show the trend on this machine only.
Threads synchronized AtomicInteger LongAdder
1 103 ms 164 ms 205 ms
2 324 ms 405 ms 227 ms
4 700 ms 376 ms 320 ms
8 641 ms 435 ms 472 ms
In all 4 runs, AtomicInteger was faster than synchronized at 4 and 8 threads. LongAdder was not clearly faster than AtomicInteger here, because with 2 CPUs at most two threads update the counter at the same moment, so contention stays low. On a machine with many cores, the Javadoc trade-off applies, so we run CounterContentionBenchmark from the source code on our own hardware before switching.
6. setPlain(), lazySet() and setRelease()
The method set() writes the value with the same memory effect as a volatile write, so other threads see the new value at once. AtomicInteger also has weaker writes that skip some of the memory ordering for speed.
- The methods lazySet(v) and setRelease(v) (Java 9+) both write v with release ordering, as defined for VarHandle. The writes before the release write become visible no later than the new value, but other threads may see the new value a little later than with set().
- The methods setPlain(v) and getPlain() (Java 9+) read and write the value as if the field were a normal, non-volatile field, so other threads have no visibility guarantee.
likes.lazySet(1); // value = 1
likes.setRelease(2); // value = 2
likes.setPlain(3); // value = 3
int plain = likes.getPlain(); // plain = 3
Application code rarely needs these methods. Libraries use lazySet() or setPlain() in hot paths, for example to reset a counter that only one thread touches at that moment. In regular code, we use set() and get().
7. AtomicInteger FAQs
7.1. Is AtomicInteger Thread-Safe?
Yes. Every method of AtomicInteger is atomic, so many threads can call incrementAndGet(), compareAndSet() or updateAndGet() on the same object without a lock. A sequence of two calls, such as get() followed by set(), is not atomic, so we replace it with compareAndSet() or updateAndGet().
7.2. What Happens When AtomicInteger Overflows?
The value wraps around, the same as an int. AtomicInteger does not throw an exception, so calling incrementAndGet() on Integer.MAX_VALUE returns a negative number.
AtomicInteger max = new AtomicInteger(Integer.MAX_VALUE);
int wrapped = max.incrementAndGet(); // wrapped = -2147483648
For counters that can pass about 2.1 billion, we use AtomicLong or LongAdder. To fail on overflow instead, we call updateAndGet(Math::incrementExact), which throws ArithmeticException.
7.3. Why Does equals() Return false for Two AtomicInteger Objects With the Same Value?
AtomicInteger does not override equals(), so equals() compares object identity, not values. For example, new AtomicInteger(5).equals(new AtomicInteger(5)) returns false. To compare values, we compare a.get() == b.get().
7.4. What Is the Difference Between AtomicInteger and AtomicLong?
AtomicLong has the same methods as AtomicInteger, but the value is a long. We use AtomicLong when the value can pass the int range, for example a total of bytes or a timestamp in milliseconds.
8. Conclusion
AtomicInteger makes updates of a single int atomic, so counters shared between threads never lose updates. We use incrementAndGet() and addAndGet() for counters, compareAndSet() or updateAndGet() when the new value depends on a condition, and keep the update functions free of side effects.
When two or more values must change together, we use synchronized or a Lock instead. For counters that many threads update often and read rarely, LongAdder scales better on machines with many cores.
9. References
- AtomicInteger Javadoc (Java 25)
- LongAdder Javadoc (Java 25)
- java.util.concurrent.atomic package summary (Java 25)
- VarHandle Javadoc (Java 25)
Happy Learning !!