Java Tutorial
🔍

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.

1// File: BeforeAtomicVariables.java 2import java.util.concurrent.CountDownLatch; 3 4public class BeforeAtomicVariables { 5 6 static int counter = 0; 7 static final Object LOCK = new Object(); 8 9 public static void main(String[] args) throws InterruptedException { 10 int threadCount = 4; 11 CountDownLatch done = new CountDownLatch(threadCount); 12 13 for (int i = 0; i < threadCount; i++) { 14 new Thread(() -> { 15 for (int j = 0; j < 10_000; j++) { 16 synchronized (LOCK) { 17 counter++; 18 } 19 } 20 done.countDown(); 21 }).start(); 22 } 23 24 done.await(); 25 System.out.println("Final count: " + counter); 26 } 27}
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.

1// File: AfterAtomicVariables.java 2import java.util.concurrent.CountDownLatch; 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class AfterAtomicVariables { 6 7 static AtomicInteger counter = new AtomicInteger(); 8 9 public static void main(String[] args) throws InterruptedException { 10 int threadCount = 4; 11 CountDownLatch done = new CountDownLatch(threadCount); 12 13 for (int i = 0; i < threadCount; i++) { 14 new Thread(() -> { 15 for (int j = 0; j < 10_000; j++) { 16 counter.incrementAndGet(); 17 } 18 done.countDown(); 19 }).start(); 20 } 21 22 done.await(); 23 System.out.println("Final count: " + counter.get()); 24 } 25}
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.

1// File: AtomicMethodsExample.java 2import java.util.concurrent.atomic.*; 3 4public class AtomicMethodsExample { 5 public static void main(String[] args) { 6 AtomicInteger counter = new AtomicInteger(10); 7 8 System.out.println("getAndIncrement: " + counter.getAndIncrement()); 9 System.out.println("After getAndIncrement: " + counter.get()); 10 11 System.out.println("incrementAndGet: " + counter.incrementAndGet()); 12 13 boolean swapped = counter.compareAndSet(12, 100); 14 System.out.println("compareAndSet(12, 100): " + swapped); 15 System.out.println("Value after CAS: " + counter.get()); 16 17 int updated = counter.updateAndGet(v -> v * 2); 18 System.out.println("updateAndGet(v -> v*2): " + updated); 19 } 20}
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.

1// File: AtomicReferenceExample.java 2import java.util.concurrent.atomic.AtomicReference; 3 4public class AtomicReferenceExample { 5 6 record Config(String environment, int retries) {} 7 8 public static void main(String[] args) { 9 AtomicReference<Config> currentConfig = new AtomicReference<>(new Config("staging", 3)); 10 11 Config old = currentConfig.getAndSet(new Config("production", 5)); 12 13 System.out.println("Old config: " + old); 14 System.out.println("New config: " + currentConfig.get()); 15 } 16}
Output:
Old config: Config[environment=staging, retries=3]
New config: Config[environment=production, retries=5]

Real-World Example

A website tracks page views per page using separate atomic counters, safely incremented by many concurrent request-handling threads with no lock involved at all — the most common use case for these classes by far, alongside lock-free boolean flags via AtomicBoolean and the lock-free retry-loop pattern this section's Race Conditions article uses to safely enforce a coupon stock limit.

1// File: PageViewCounter.java 2import java.util.concurrent.atomic.AtomicLong; 3 4public class PageViewCounter { 5 private final AtomicLong homePageViews = new AtomicLong(); 6 private final AtomicLong productPageViews = new AtomicLong(); 7 8 public void recordHomePageView() { 9 homePageViews.incrementAndGet(); 10 } 11 12 public void recordProductPageView() { 13 productPageViews.incrementAndGet(); 14 } 15 16 public long getHomePageViews() { 17 return homePageViews.get(); 18 } 19 20 public long getProductPageViews() { 21 return productPageViews.get(); 22 } 23}
1// File: PageViewCounterDemo.java 2import java.util.concurrent.CountDownLatch; 3 4public class PageViewCounterDemo { 5 public static void main(String[] args) throws InterruptedException { 6 PageViewCounter counter = new PageViewCounter(); 7 int requestThreads = 8; 8 int requestsPerThread = 500; 9 CountDownLatch done = new CountDownLatch(requestThreads); 10 11 for (int i = 0; i < requestThreads; i++) { 12 final int threadIndex = i; 13 new Thread(() -> { 14 for (int j = 0; j < requestsPerThread; j++) { 15 if (threadIndex % 2 == 0) { 16 counter.recordHomePageView(); 17 } else { 18 counter.recordProductPageView(); 19 } 20 } 21 done.countDown(); 22 }).start(); 23 } 24 25 done.await(); 26 27 System.out.println("Home page views: " + counter.getHomePageViews()); 28 System.out.println("Product page views: " + counter.getProductPageViews()); 29 } 30}
Output:
Home page views: 2000
Product page views: 2000

A mistake that appears often in fresher pull requests is reaching for a synchronized block to guard a simple counter like this, adding lock acquisition overhead to every single page view recorded, when the counter itself is the only shared state involved. AtomicLong's incrementAndGet(), used throughout PageViewCounter here, gives the exact same correctness guarantee synchronized would for this specific case, without ever actually blocking a thread.

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.

1// File: IgnoredCompareAndSetMistake.java 2import java.util.concurrent.atomic.AtomicInteger; 3 4public class IgnoredCompareAndSetMistake { 5 public static void main(String[] args) { 6 AtomicInteger value = new AtomicInteger(5); 7 8 // Another thread (simulated here directly) changes the value first 9 value.set(99); 10 11 boolean succeeded = value.compareAndSet(5, 10); 12 System.out.println("CAS succeeded: " + succeeded); 13 System.out.println("Value: " + value.get()); 14 } 15}
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.

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

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