Atomic classes live in java.util.concurrent.atomic and they exist for one job: let many threads update a single shared variable correctly, without a lock. They wrap a value (an int, a long, a boolean, a reference, an array slot) and expose operations that the JVM compiles down to a single hardware instruction. That instruction is what makes the update safe under contention. This lesson covers the main classes (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference, AtomicIntegerArray), the compare-and-swap (CAS) operation that powers them, the higher-level helpers (updateAndGet, accumulateAndGet), the high-contention specialists (LongAdder, LongAccumulator), and the ABA problem with its fix (AtomicStampedReference).
Here, the focus is on the lock-free, single-variable case where atomics are well-suited.
Consider an online store that needs a globally unique order ID for every order it accepts. Each request handler runs on a thread from a thread pool, and the store gets hundreds of orders per second during a sale. The naive code looks innocent.
The expected total is 200000. The actual number is somewhere lower and changes on every run. The line nextOrderId = nextOrderId + 1 looks like one step but it's three: read the field, add one, write the new value back. Two threads can read the same value, both add one, both write the same result, and a whole increment disappears.
You could wrap the body in synchronized and the result would be correct. But for a single-field update, taking a lock is a heavy hammer. The thread that loses the race for the lock is parked, the OS scheduler gets involved, and you pay the cost of context switches that have nothing to do with adding one to an integer. Atomic classes do the same job with a hardware instruction instead of a lock.
The total is exact every time. The single call incrementAndGet() is what changed. It reads, adds one, and writes, and the JVM guarantees no other thread can sneak between those steps. No synchronized block, no lock object.
Two threads enter incrementAndGet, and the CPU funnels them through a single hardware instruction that updates the value. One thread succeeds on the first try. The other tries again, very fast, and succeeds on the next attempt. Nobody is parked, nobody waits on a lock. The total is exact, and the program never sleeps.
Every atomic operation rests on one primitive: compare-and-swap, abbreviated CAS. The idea is straightforward. You give the CPU three things: a memory location, the value you think is there, and the value you want to put there. The CPU does both the compare and the swap as one indivisible step. If the location still holds the value you expected, the CPU writes the new value and reports success. If anything else is there, the CPU does nothing and reports failure.
The Java surface for this is AtomicInteger.compareAndSet(int expected, int update). Here it is in isolation.
The second compareAndSet expected 10, but the value was already 9. The second call fails.
The first swap expected 10 and found 10, so it wrote 9 and returned true. The second swap expected 10 and found 9, so it didn't touch the value and returned false. The stock stayed at 9.
On its own, one compareAndSet looks underwhelming. The power comes from the CAS loop: read the current value, compute the new one, try to swap, and if the swap fails, start over. The loop is bounded by how often other threads beat you to it. In low contention, you usually succeed on the first try. In high contention, you spin a few times. Either way, you never block.
The cycle in the diagram is the CAS loop. The only path that exits is the success leg. The retry leg goes back to read, recompute, and try again with the freshest value. This is what incrementAndGet() is doing internally: a tight CAS loop that adds one.
On x86 hardware, CAS compiles to lock cmpxchg. On ARM, it's a load-linked / store-conditional pair. The JVM picks the right instruction for the platform, and the operation is roughly as cheap as a memory-barrier read. No kernel call, no lock acquisition, no thread parking.
A single CAS is fast, but a CAS that keeps failing is not. Under heavy contention from many threads on one variable, threads can spin through dozens of retries before they succeed. At that point, a plain synchronized block that parks contending threads can outperform the atomic.
get then set?Writing an "atomic" increment by hand using get and set assumes the pair is safe because both methods sit on an atomic class. It isn't. Each call is individually atomic, but the pair isn't.
What's wrong with this code?
Output (a typical run):
The number is wrong, in the same way as the very first plain-int example. The counter.set(counter.get() + 1) line is three operations: read with get(), add one, write with set(). Between the read and the write, another thread can do the same thing. Both threads read 5, both compute 6, both write 6, and one increment vanishes.
Fix:
incrementAndGet is a single CAS-based step. Internally it loops, but each loop iteration is one atomic compare-and-swap, so the increment is never lost. The lesson: using an AtomicInteger doesn't make all sequences of calls atomic, only individual calls. Any time you read-modify-write, use the bundled method (incrementAndGet, getAndAdd, updateAndGet), not get followed by set.
AtomicInteger and AtomicLong SurfaceAtomicInteger is the most commonly used atomic. AtomicLong is the same shape with a long instead. Both expose a focused set of operations, and most of the lesson's later content uses these methods as building blocks. Here's the surface that matters.
| Method | What it does | Return value |
|---|---|---|
get() | Read the current value | The current value |
set(int v) | Write v unconditionally | void |
incrementAndGet() | Add 1, return the new value | New value |
getAndIncrement() | Add 1, return the previous value | Previous value |
decrementAndGet() | Subtract 1, return the new value | New value |
addAndGet(int delta) | Add delta, return the new value | New value |
getAndAdd(int delta) | Add delta, return the previous value | Previous value |
compareAndSet(int expected, int update) | CAS, returns whether the swap happened | boolean |
updateAndGet(IntUnaryOperator f) | Apply f, retry on contention, return the new value | New value |
accumulateAndGet(int x, IntBinaryOperator f) | Apply f(current, x), retry, return new value | New value |
The "get-and-X" vs "X-and-get" naming is the same idea you've seen with i++ vs ++i. "Get-and-increment" returns the old value (post-increment); "increment-and-get" returns the new one (pre-increment). Use whichever fits the call site.
The following program exercises several of these on an order-ID counter.
Output:
get returns whatever the value currently is. getAndIncrement returns the prior value (1000) and leaves 1001 behind. incrementAndGet returns the new value (1002). addAndGet(10) adds 10 and returns the new value (1012). The first compareAndSet succeeds because the value is 1012. The second fails because the value is now 2000.
updateAndGet and accumulateAndGetincrementAndGet and addAndGet cover the common cases, but sometimes you want a richer update. Maybe you want to clamp the value to a maximum, or apply a non-linear function. updateAndGet takes a function and applies it inside a CAS loop. accumulateAndGet is the two-argument cousin: it takes an extra value and a function that combines it with the current state.
updateAndGet runs Math.max(0, current - 30) inside a CAS loop. If another thread races in between the read and the write, the loop retries with the new current value. The clamp at zero is part of the function, so even under contention nobody can drive stock negative.
accumulateAndGet takes the same idea and adds a second argument. The 40 is the delta passed in, and the function combines it with the current value. The cap at 100 lives in the function. You'd use this when the "thing to add" is a per-call value, but the combining rule is fixed.
One detail matters: the function you pass must be side-effect free and idempotent on retry. The CAS loop may run it more than once if there's contention. If your function increments a different counter, makes a network call, or otherwise has side effects, those side effects happen on every retry. That's almost always wrong.
Output (single-threaded, no contention):
In single-threaded code the function runs once, so the bad pattern looks fine. Add contention and the same code calls the function multiple times per logical increment, and the retryCount field overcounts. Keep updater functions pure: they read current, return a new value, and do nothing else.
AtomicBoolean: One-Shot FlagsAtomicBoolean wraps a boolean. It exists for the same reason as AtomicInteger: you want a flag that many threads can read and write without confusion. The most common use is a one-shot guard, the kind of "I want this initializer to run exactly once" pattern.
Output (one possible run):
Whichever thread wins the CAS race runs the initialization. The other two see false -> true fail (because the value is already true), and they take the "skip" branch. The pattern is compareAndSet(false, true) to claim the flag, and the boolean return tells you whether you won.
The same shape works for shutdown flags, "has this work been claimed?" checks, and any other state that goes from one value to another exactly once. Don't use a plain boolean for this. The plain field would let two threads both pass an if check between the read and the write.
AtomicReference for Hot-Swappable ObjectsAtomics aren't limited to numbers. AtomicReference<T> wraps an object reference and lets you swap it atomically. The classic use case is a configuration or pricing snapshot that you want to replace whole, without locking, while threads keep reading.
In an e-commerce store, the pricing service might keep an in-memory price list. A background job rebuilds the list every minute and swaps the new version in. Hundreds of request threads are reading the current list at the same time. Readers should never block, and the writer should atomically publish the new list.
Each call to priceOf does a single current.get() to grab the current PriceList reference. Whatever happens after that point reads through that one snapshot, even if a publisher swaps in a new list mid-call. Different threads can see different snapshots for a moment after a publish, which is fine: every snapshot is internally consistent because it's an immutable object.
The published PriceList must be immutable for this to work. If readers can mutate the underlying Map, you've lost the safety. The pattern works because the writer assembles a fresh, immutable object, and the swap publishes it whole.
AtomicReference also has compareAndSet, updateAndGet, and accumulateAndGet versions, exactly like AtomicInteger. Use the CAS version if multiple threads might publish a new version and each one needs to be built from the most recent state.
Each call to updateAndGet reads the current Cart, builds a new one with one more item, and CAS-swaps it in. If two threads race, one wins and the other retries with the new state. The Cart record is immutable, so even concurrent readers always see a coherent snapshot.
Every updateAndGet on an AtomicReference allocates a new object (the result of the function). For a high-frequency code path, that allocation pressure can matter. A mutable single-instance object protected by a lock can be cheaper when contention is low and the object is large.
AtomicIntegerArray for Per-Slot AtomicitySometimes you want a whole array of counters, and you want each slot to be independently atomic. You could use an AtomicInteger[], where each element is its own object, but that's a lot of small objects and a lot of indirection. AtomicIntegerArray packs int slots directly and gives you atomic methods on each index.
Consider per-category page-view counters for a storefront. There's no single hot counter; the load is spread across hundreds of categories.
Each call to views.incrementAndGet(i) does a CAS on slot i only. Two threads incrementing different slots don't contend with each other at all. Two threads incrementing the same slot do contend, but only on that one slot. The class also has compareAndSet(index, expected, update), getAndAdd(index, delta), and the rest of the familiar suite, indexed by slot.
AtomicLongArray and AtomicReferenceArray<T> are the same shape for long and reference types. Use them when you have a fixed-size collection of independent atomic cells.
CAS is fast when most threads succeed on the first try. It hurts when many threads are hammering one variable and most CAS attempts fail. Each failed attempt does a memory read, computes a new value, attempts the swap, and goes back to the start. With ten threads all incrementing the same AtomicLong, most of the CPU time can end up in retries instead of useful work.
A page-view tracker is a standard case. Every request increments a global counter. At peak, you might have eight CPUs all trying to increment one AtomicLong, and the cache line holding that long is bouncing between cores. Every other CAS fails because the line keeps getting invalidated.
LongAdder and LongAccumulator, both added in Java 8, solve this with a different design. Instead of one cell, they keep an array of cells. Each thread updates whichever cell is least contended (in practice, a cell chosen by the thread's hash). When you want the total, you sum the cells.
Output (one possible run on an 8-core machine):
Both counters end at 8,000,000. The LongAdder finished in about a fifth of the time. Numbers vary by machine and JVM, but the shape is consistent: under high contention, LongAdder wins comfortably. Under low contention (one thread incrementing), AtomicLong is usually slightly faster because it doesn't have the per-cell overhead. The two diagrams below show the difference in shape.
The AtomicLong shape is a funnel: every thread targets one memory location, and contention is whatever the slowest cache-coherence step costs times the number of competing threads.
The LongAdder shape spreads writes across cells. Each thread mostly hits its own cell, so the cache lines don't bounce. Reading the total is slower (you sum all the cells), but writes are the hot path, and writes are what high-throughput counters care about.
The trade-off is right there in the diagram. Use LongAdder when you write often and read rarely (metrics, page views, request counts). Avoid it when you need a precise, current value on every read (a remaining-stock counter that gates "out of stock" decisions, where reading and acting on the value has to be atomic). For that, use AtomicLong or a lock.
LongAccumulator is LongAdder's generalization. Instead of always adding, you pass in a LongBinaryOperator that combines the new value with the accumulated state. Use it when the combining rule isn't +, for example tracking the running maximum response time.
Long::max is the combiner. The starting state is 0. Each accumulate updates the per-cell value with max(currentCell, newValue). When you call get, it reduces across cells with the same operator. The result is a running maximum that scales the same way LongAdder does, without lock contention on a single cell.
CAS has one famous wrinkle. The compare step checks "does the value equal X?", not "has the value been X the entire time?". If another thread changes the value from A to B and then back to A, the CAS still sees A and succeeds, even though the world wasn't stable during the read-modify-write window. This is the ABA problem.
For an int counter that only ever increments, ABA doesn't apply. The value never goes backwards. For a reference to a node in a lock-free linked list, ABA can cause real bugs: another thread might pop a node, push it again, and your CAS happily proceeds based on a stale assumption about what the node points to.
The diagram shows the timeline. Thread 1 read A, paused. Thread 2 changed the value to B, then back to A. Thread 1 wakes up, runs its CAS expecting A, finds A, and proceeds as if nothing had happened. For some workloads, that's fine. For others (especially anything involving recycled objects), it's a bug.
AtomicStampedReference<V> is the fix. It pairs the reference with an integer "stamp" that increments on every update. The CAS checks both the reference and the stamp. If the stamp has moved, the CAS fails, even if the reference came back to its original value.
The final CAS expected the reference "A" and the stamp 1. The reference is "A" (so a plain compareAndSet on an AtomicReference would have succeeded), but the stamp is 3. The check on the stamp catches the change history and the CAS returns false. The state stays at "A" with stamp 3, and the failed thread can re-read and decide what to do.
The stamp doesn't have to be a counter. It can be any int that you guarantee to change on every meaningful update. In practice, monotonically incrementing is the simplest pattern and avoids accidental stamp reuse.
For pure numeric atomics, ABA isn't a concern. It arises when implementing lock-free data structures (Treiber stacks, Michael-Scott queues), where nodes can be recycled. If you find yourself building one of those, use AtomicStampedReference or its cousin AtomicMarkableReference.
synchronized: When to Pick WhichBy now you've seen the two main approaches to safe shared state: lock-based (with synchronized or ReentrantLock) and lock-free (with atomics). The choice isn't aesthetic; it depends on what you're protecting.
| Situation | Lock-based | Atomic |
|---|---|---|
| Updating one variable | Works, but heavy | Best fit |
| Coordinating updates to multiple variables | Best fit | Awkward; you'd need an AtomicReference to a snapshot object |
| Critical section with several statements | Best fit | Doesn't apply; atomics are per-operation |
| High contention on one counter | Works, but parks threads | Use LongAdder |
| Hot-swapping immutable state | Awkward | AtomicReference |
| Need to wait until a condition is true | Best fit (use wait/notify or Condition) | Doesn't apply; atomics never block |
| Want zero blocking | Doesn't apply | Best fit |
The general rule of thumb: if you're updating one variable, an atomic is almost always appropriate. If you're updating two or more variables together (a balance and a transaction log, say), a lock is usually cleaner, because the lock can guard the whole multi-step update. You could use an AtomicReference to a record holding both values, but you have to be willing to allocate a new record on every change.
The other consideration is correctness under retry. Atomics achieve safety by retrying CAS until it succeeds. Locks achieve safety by serializing access. Retry is cheap when the protected work is small (an int increment) and expensive when the protected work is large (rebuilding a 10 MB object). Locks scale better to large critical sections; atomics scale better to small ones.
A short note for completeness, since the volatile lesson covered this. Every atomic class internally uses a volatile field (AtomicInteger has a private volatile int value). That means writes to an atomic are visible to other threads as soon as they complete, and reads see the most recent write. You get the visibility guarantees of volatile plus the atomicity of CAS, in one package.
The practical impact: when one thread does atomicCounter.set(42) and another thread does atomicCounter.get(), the second thread is guaranteed to see 42 (or any value written later). That's something synchronized also gives you, but without the visibility cost of acquiring a monitor. Atomics give you visibility almost free, on top of the atomicity.
This is why a plain volatile long with ++ is broken: volatile gives you visibility, but ++ is still three steps, and two of them aren't atomic. Atomics give you visibility and atomic compound operations. You need both.
A short program that pulls the major pieces together: an AtomicInteger for order IDs, an AtomicReference for the current price list, and a LongAdder for page views. Multiple threads exercise each one.
Output (numbers depend on the schedule):
Three threads run concurrently. Each one calls priceOf (which reads prices and bumps pageViews) and occasionally calls placeOrder (which bumps pageViews and nextOrderId). The main thread, in parallel, swaps in a new price list. The final order ID and page-view total are exact: no missed increments, despite hundreds of thousands of concurrent updates. The price list version on the readers' side is 2 because the publish happened during the run.
Each atomic in the program is doing the job it's best at. AtomicInteger for a monotonic ID. AtomicReference for a hot-swap of an immutable snapshot. LongAdder for a high-throughput counter that's read infrequently. No synchronized block anywhere, and no threads parked waiting for locks.
10 quizzes