Java AtomicInteger Tutorial: Methods, CAS and LongAdder

AtomicInteger lets many threads update one int without a lock. Covers every method with results, compare-and-swap, AtomicInteger vs synchronized vs volatile, and LongAdder for high contention.

Java Concurrency

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.

MethodWhat it doesReturns
get()Reads the current valuethe value
set(v)Writes vnothing
getAndSet(v)Writes vthe old value
incrementAndGet()Adds 1the new value, like ++i
getAndIncrement()Adds 1the old value, like i++
decrementAndGet(), getAndDecrement()Subtracts 1the new or the old value
addAndGet(delta), getAndAdd(delta)Adds delta, which can be negativethe new or the old value
compareAndSet(expected, v)Writes v only if the value equals expectedtrue 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.

Sequence diagram of two threads adding 1 to downloads. Both read 5 and compute 6. Thread A calls compareAndSet(5, 6), which succeeds and sets the value to 6. Thread B calls compareAndSet(5, 6), which fails because the value is now 6. Thread B reads 6 again, computes 7, and compareAndSet(6, 7) succeeds.
The failed compareAndSet() tells thread B to retry, so no update is lost and no thread waits.

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 intAtomicIntegersynchronized block
Other threads see the latest valueyesyesyes, when they also synchronize
count++ is safenoyes, incrementAndGet()yes
Check-then-act on one valuenoyes, compareAndSet() or updateAndGet()yes
Update two or more fields togethernonoyes
Threads wait for each othernono, a failed CAS retriesyes, waiting threads are blocked
Typical usea flag such as runningcounters, sequence numbers, statisticsinvariants 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.

Two panels. Left: four threads T1 to T4 all point to one AtomicInteger value, and many CAS calls fail and retry under contention; get() returns the exact value. Right: four threads write to four separate LongAdder cells, and sum() adds the cells; sum() is not an atomic snapshot.
AtomicInteger keeps one value, whereas LongAdder spreads contended updates over cells and adds them up in sum().

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

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.