Java Tutorial
🔍

Java Atomic Variables

Atomic Variables

AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference<V>, all in java.util.concurrent.atomic, provide thread-safe operations on a single variable without ever acquiring a traditional lock — built instead on compare-and-swap, a hardware-supported operation that avoids the overhead of blocking a thread entirely in the common case.

What Are Atomic Variables?

The atomic classes wrap a single value and expose operations — incrementAndGet(), compareAndSet(), updateAndGet() — that read, modify, and write it back as one indivisible step, with no lock ever acquired. They live in java.util.concurrent.atomic, a sibling package to java.util.concurrent itself, both introduced in Java 5.

Why Thread Safety Matters Here

A synchronized counter is correct, but every single increment pays the cost of acquiring and releasing a lock, and that cost adds up fast on a hot path like a request counter or a shared accumulator hit by dozens of threads at once. Without atomicity, a plain int shared across threads loses updates silently — two threads reading the same value before either writes back their increment is exactly how a counter ends up under its expected total, with no exception thrown to flag it.

How It Works

One sentence before the diagram: compare-and-swap is the single hardware-level operation every atomic method builds on.

Thread wants to update value from 5 to 6:

  1. read current value       -->  5
  2. compare to expected (5)  -->  matches
  3. swap to new value (6)    -->  succeeds, returns true

If another thread changed it to 9 first:

  1. read current value       -->  9
  2. compare to expected (5)  -->  does not match
  3. no swap                  -->  fails, returns false, caller retries

The atomic classes never block a thread. A failed compare-and-swap simply returns false — it is the caller's responsibility to check that return value and retry if the operation genuinely needs to succeed.

Code Examples

A synchronized counter is correct, but every single increment pays the cost of acquiring and releasing a lock.

// File: BeforeAtomicVariables.java import java.util.concurrent.CountDownLatch; public class BeforeAtomicVariables { static int counter = 0; static final Object LOCK = new Object(); public static void main(String[] args) throws InterruptedException { int threadCount = 4; CountDownLatch done = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { new Thread(() -> { for (int j = 0; j < 10_000; j++) { synchronized (LOCK) { counter++; } } done.countDown(); }).start(); } done.await(); System.out.println("Final count: " + counter); } }
Output:
Final count: 40000

AtomicInteger reaches the same guaranteed-correct result without a lock object or a synchronized block at all — incrementAndGet() performs the entire read-modify-write sequence as one atomic step internally.

// File: AfterAtomicVariables.java import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class AfterAtomicVariables { static AtomicInteger counter = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { int threadCount = 4; CountDownLatch done = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { new Thread(() -> { for (int j = 0; j < 10_000; j++) { counter.incrementAndGet(); } done.countDown(); }).start(); } done.await(); System.out.println("Final count: " + counter.get()); } }
Output:
Final count: 40000

Both reach exactly 40000 — the difference is that the atomic version never blocks a thread to get there, relying instead on compare-and-swap, a single hardware-supported instruction that applies an update only if the value has not changed since it was read.

Beyond simple increments, the atomic classes support reading-then-modifying, conditional replacement, and functional updates.

// File: AtomicMethodsExample.java import java.util.concurrent.atomic.*; public class AtomicMethodsExample { public static void main(String[] args) { AtomicInteger counter = new AtomicInteger(10); System.out.println("getAndIncrement: " + counter.getAndIncrement()); System.out.println("After getAndIncrement: " + counter.get()); System.out.println("incrementAndGet: " + counter.incrementAndGet()); boolean swapped = counter.compareAndSet(12, 100); System.out.println("compareAndSet(12, 100): " + swapped); System.out.println("Value after CAS: " + counter.get()); int updated = counter.updateAndGet(v -> v * 2); System.out.println("updateAndGet(v -> v*2): " + updated); } }
Output:
getAndIncrement: 10
After getAndIncrement: 11
incrementAndGet: 12
compareAndSet(12, 100): true
Value after CAS: 100
updateAndGet(v -> v*2): 200

getAndIncrement() returns the value before incrementing; incrementAndGet() returns the value after. compareAndSet(expected, newValue) only applies the update if the current value still equals expected. updateAndGet() applies an arbitrary function to the current value atomically, useful for updates more involved than a simple increment.

AtomicReference<V> extends the same idea to object references, letting an entire immutable object be swapped out atomically.

// File: AtomicReferenceExample.java import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceExample { record Config(String environment, int retries) {} public static void main(String[] args) { AtomicReference<Config> currentConfig = new AtomicReference<>(new Config("staging", 3)); Config old = currentConfig.getAndSet(new Config("production", 5)); System.out.println("Old config: " + old); System.out.println("New config: " + currentConfig.get()); } }
Output:
Old config: Config[environment=staging, retries=3]
New config: Config[environment=production, retries=5]

Basic Example

Two threads share a single AtomicInteger counter and each increment it many times with no lock in sight; joining both threads before printing keeps the final output deterministic.

import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) throws InterruptedException { AtomicInteger counter = new AtomicInteger(0); Runnable incrementTask = () -> { for (int i = 0; i < 1000; i++) { counter.incrementAndGet(); } }; Thread t1 = new Thread(incrementTask); Thread t2 = new Thread(incrementTask); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + counter.get()); } }

Output

Final count: 2000

How it works:

  1. ›counter.incrementAndGet() reads, increments, and writes back the value as one indivisible compare-and-swap step, so no update from either thread is ever lost.
  2. ›Both threads call incrementAndGet() 1000 times each, and no lock or synchronized block is needed to keep the result correct.
  3. ›t1.join() and t2.join() wait for both threads to finish before the final count is printed, which is what makes 2000 the only possible output every time this runs.

Best Practices

Reach for an atomic class the moment shared state is a single variable needing a simple update — an increment, a swap, a conditional replace — rather than defaulting to synchronized for something this narrow.

Always check compareAndSet()'s return value, and loop to retry on failure when the operation genuinely needs to succeed eventually, rather than assuming a single call will always apply.

Use AtomicReference<V> to swap an entire immutable object atomically instead of updating several related fields separately, which reintroduces the same kind of inconsistency window locking is meant to avoid.

Reach for synchronized or Lock instead of stacking several atomic variables together the moment an operation genuinely needs more than one piece of shared state to change consistently as a unit. volatile, covered in this section's dedicated article, is the visibility mechanism the atomic classes build on internally — each one stores its value in a volatile field and layers compare-and-swap on top of it for atomicity, and ConcurrentHashMap, covered next in this section, extends this same lock-free philosophy to per-key operations on an entire map.

Common Mistakes

Ignoring compareAndSet()'s return value assumes the update always succeeds — it does not, and a failed call silently leaves the value unchanged rather than throwing.

// File: IgnoredCompareAndSetMistake.java import java.util.concurrent.atomic.AtomicInteger; public class IgnoredCompareAndSetMistake { public static void main(String[] args) { AtomicInteger value = new AtomicInteger(5); // Another thread (simulated here directly) changes the value first value.set(99); boolean succeeded = value.compareAndSet(5, 10); System.out.println("CAS succeeded: " + succeeded); System.out.println("Value: " + value.get()); } }
Output:
CAS succeeded: false
Value: 99

The value had already changed to 99 by the time compareAndSet(5, 10) ran, so it correctly refused to apply — code that ignores this false return and assumes the update happened would be silently wrong.

Assuming two separate atomic variables updated "together" form one atomic transaction is a second mistake — each variable's own update is atomic individually, but nothing ties the two together, so a reader can still observe one already updated and the other not yet.

// Illustrative only - each individual atomic update is safe, but reading // both values together is not a single atomic snapshot. Another thread // could update x between reading it and reading y, showing an inconsistent // combined view of the two. import java.util.concurrent.atomic.AtomicInteger; public class MultiAtomicMistake { static final AtomicInteger x = new AtomicInteger(0); static final AtomicInteger y = new AtomicInteger(0); static void printBoth() { System.out.println(x.get() + ", " + y.get()); } }

Interview Questions

Q1. What is compare-and-swap (CAS), and how does it let AtomicInteger avoid locking?

CAS is a hardware-supported operation that atomically replaces a value with a new one only if it still equals an expected value — if it does not, the operation fails without applying the change. Because this check-and-replace happens as one indivisible CPU-level step, the atomic classes can guarantee correctness without ever blocking a thread to acquire a lock. Interviewers are listening for whether you understand this is a hardware instruction, not a JVM-level trick.

Q2. What is the difference between incrementAndGet() and getAndIncrement()?

incrementAndGet() increments the value and returns the result after incrementing. getAndIncrement() returns the value before incrementing, then applies the increment — the same distinction as prefix ++x versus postfix x++. This is a quick recall check, but a strong answer connects it to the prefix/postfix analogy unprompted.

Q3. What happens if compareAndSet() is called with an expected value that no longer matches?

The call returns false and leaves the current value unchanged — it does not throw an exception, exactly as demonstrated in this article's IgnoredCompareAndSetMistake example. The nuance interviewers want is knowing the caller must check the return value themselves.

Q4. Are operations across multiple different Atomic variables guaranteed to be atomic together?

No. Each individual atomic variable's own operations are atomic, but there is no guarantee tying updates or reads across two or more different atomic variables together as a single unit — a thread can observe one already changed and the other not yet. This is exactly the trap this article's MultiAtomicMistake illustrates.

Q5. Is AtomicInteger faster than a synchronized counter under all conditions?

Generally yes for low to moderate contention, since it avoids blocking entirely. Under extremely high contention from many threads, repeated failed compare-and-swap retries can themselves become a bottleneck, which is why classes like LongAdder exist for that specific high-contention scenario. Product-company interviewers often push specifically on this high-contention edge case.

Q6. What does updateAndGet() do, and how does it differ from a plain compareAndSet() call?

updateAndGet() applies a given function to the current value and atomically stores the result, internally retrying with a compare-and-swap loop if the value changed during the computation — it handles the retry logic automatically, unlike a single compareAndSet() call, which the caller must retry manually if it fails.

Q7. Can an AtomicReference hold a null value?

Yes, AtomicReference<V> places no restriction on holding null — it behaves like any other reference-typed variable in that respect, just with atomic get, set, and compare-and-swap operations layered on top.

FAQs

Is java.util.concurrent.atomic part of the same package as ExecutorService?

They are sibling packages — ExecutorService lives in java.util.concurrent directly, while the atomic classes live in the java.util.concurrent.atomic subpackage, both introduced together in Java 5.

Does AtomicInteger extend Number?

Yes, AtomicInteger and AtomicLong both extend Number, providing intValue(), longValue(), doubleValue(), and similar conversion methods for interoperating with code that expects a plain numeric type.

Is there an AtomicBoolean, and what is it typically used for?

Yes, AtomicBoolean provides the same get/set/compareAndSet operations for a single boolean value, typically used as a lock-free flag — for example, ensuring some action happens exactly once across multiple threads via compareAndSet(false, true).

Can AtomicLong overflow the same way a plain long can?

Yes, AtomicLong wraps around using the same two's-complement overflow behavior as a plain long — there is no built-in overflow protection.

Is volatile involved in how the atomic classes work internally?

Yes, each atomic class stores its value in a volatile field internally, layering compare-and-swap operations on top of it to add atomicity beyond the visibility volatile alone provides.

Is AtomicInteger appropriate for very high-contention counters used by hundreds of threads?

It works correctly, but under very high contention, repeated failed compare-and-swap attempts can reduce throughput. LongAdder, added in Java 8 specifically for high-contention counting, splits the count across multiple internal cells to reduce that contention, at the cost of a slightly more expensive read when the total is actually needed.

Does compareAndSet() throw an exception if it fails?

No, it simply returns false — the caller is expected to check the return value and decide how to respond, typically by retrying in a loop, exactly as this section's Race Conditions article demonstrates.

Summary

The atomic classes give correctness guarantees equivalent to synchronized for operations on a single variable, without ever needing to acquire a lock — built on compare-and-swap, a hardware-supported operation that applies an update only if nothing else has changed the value first. incrementAndGet() and its relatives cover simple numeric updates, compareAndSet() and updateAndGet() cover more general conditional and functional updates, and AtomicReference<V> extends the same idea to swapping entire immutable objects.

The habit worth carrying forward from this article's page-view counter example is reaching for an atomic class the moment a single shared variable is all that needs protecting, and always checking — or looping on — compareAndSet()'s return value rather than assuming it always succeeds.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →