JAVA / THREADS AND CONCURRENCY
Atomic variables and lock-free counters
Replace lock-protected counters with AtomicInteger, and use compareAndSet or updateAndGet to build correct lock-free updates of one shared value.
What you will learn
- Swap ++ on a shared counter for incrementAndGet or getAndAdd
- Write a compareAndSet retry loop and say why a failed CAS is not an error
- Encode any single-value update rule in updateAndGet or accumulateAndGet
- Spot invariants spanning two fields that atomics cannot protect
Understanding Atomic variables and lock-free counters
A shared counter breaks because count++ is three machine steps: load the field, add one, store it back. A lock fixes that by making other threads wait, but for a single number there is a cheaper option. The classes in java.util.concurrent.atomic wrap one value and expose methods such as incrementAndGet, getAndAdd and compareAndSet that perform the whole load-add-store as one indivisible step, using the CPU's compare-and-swap instruction instead of a monitor.
The mental model is a bet, not a lock. A CAS operation says: if this memory location still holds the value X that I just read, store Y; otherwise tell me you failed. Failure is normal, not an error, so the atomic classes wrap it in a loop that re-reads the fresh value and recomputes. Nothing is ever owned, so no thread can block another and there is nothing to deadlock on; the cost of contention shows up as retries burning CPU rather than as threads parked.
The limit is scope: atomicity covers one call on one variable, not a sequence of calls and not two variables. counter.get() followed by counter.set(...) is two atomic steps with a gap in between, and an AtomicInteger count next to an AtomicLong total can be read half-updated. When the rule spans several fields you either keep a lock or put the fields in one immutable object behind an AtomicReference. In exchange, an atomic gives you volatile-strength visibility for free, so a get() always sees the last published value: one class closes both the visibility hole and the read-modify-write hole.
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeCounter {
private static final AtomicInteger hits = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread[] workers = new Thread[4];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Thread(() -> {
for (int n = 0; n < 25_000; n++) {
hits.incrementAndGet();
}
});
workers[i].start();
}
for (Thread w : workers) {
w.join();
}
System.out.println("expected = " + (workers.length * 25_000));
System.out.println("hits = " + hits.get());
AtomicInteger seats = new AtomicInteger(3);
int before = seats.getAndDecrement();
System.out.println("getAndDecrement returned " + before + ", value is now " + seats.get());
System.out.println("decrementAndGet returned " + seats.decrementAndGet());
System.out.println("CAS 1 -> 99: " + seats.compareAndSet(1, 99) + ", value " + seats.get());
System.out.println("CAS 1 -> 42: " + seats.compareAndSet(1, 42) + ", value " + seats.get());
}
}
An atomic variable turns a read-modify-write of a single memory location into one indivisible hardware step, so counters stay exact without any thread ever waiting.
Worked examples
Update rules that are not plain addition
updateAndGet and accumulateAndGet let you make a capped increment or a running maximum atomic without writing a loop.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class BoundedCounter {
public static void main(String[] args) {
AtomicInteger permits = new AtomicInteger(0);
for (int request = 0; request < 4; request++) {
int now = permits.updateAndGet(v -> v < 2 ? v + 1 : v);
System.out.println("request " + request + " -> permits " + now);
}
AtomicLong high = new AtomicLong(Long.MIN_VALUE);
for (long sample : new long[] {7, 3, 12, 9}) {
high.accumulateAndGet(sample, Math::max);
}
System.out.println("high water mark: " + high.get());
}
}
Example explained
Line 1updateAndGet(v -> v < 2 ? v + 1 : v) hides a CAS loop: it reads the value, applies the function, and retries if another thread moved the value first.
Line 2Requests 2 and 3 print 2 because the function returns v unchanged at the cap, so the CAS succeeds but stores the same number.
Line 3The lambda must be free of side effects, since a contended update can call it several times before one CAS wins.
Line 4accumulateAndGet(sample, Math::max) folds an outside operand in, turning Math.max(current, sample) into a single atomic step.
get() then set() is two steps
Forcing an interleaving with join() shows that atomicity belongs to a single method call, not to a pair of calls on an atomic field.
import java.util.concurrent.atomic.AtomicInteger;
public class GetThenSet {
public static void main(String[] args) throws InterruptedException {
AtomicInteger counter = new AtomicInteger(0);
int seen = counter.get();
Thread other = new Thread(() -> counter.incrementAndGet());
other.start();
other.join();
counter.set(seen + 1);
System.out.println("get + set, two increments: " + counter.get());
counter.set(0);
Thread a = new Thread(() -> counter.incrementAndGet());
Thread b = new Thread(() -> counter.incrementAndGet());
a.start();
b.start();
a.join();
b.join();
System.out.println("incrementAndGet, two increments: " + counter.get());
}
}
Example explained
Line 1counter.get() and counter.set(seen + 1) are each atomic, but the increment from other lands in the gap between them.
Line 2other.join() makes that interleaving happen on every run, turning a rare race into a reproducible result.
Line 3The first line prints 1 because set wrote the stale seen + 1 straight over the other thread's work.
Line 4The second pair prints 2 because incrementAndGet performs the read, the add and the write as one operation that cannot be split.
Writing the CAS loop by hand
Implementing addAndGet with compareAndSet exposes the retry loop that the atomic classes normally hide.
import java.util.concurrent.atomic.AtomicInteger;
public class HandRolledCas {
private static final AtomicInteger value = new AtomicInteger(0);
static int addByCas(int delta) {
while (true) {
int current = value.get();
int next = current + delta;
if (value.compareAndSet(current, next)) {
return next;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] workers = new Thread[3];
for (int i = 0; i < workers.length; i++) {
workers[i] = new Thread(() -> {
for (int n = 0; n < 10_000; n++) {
addByCas(1);
}
});
workers[i].start();
}
for (Thread w : workers) {
w.join();
}
System.out.println("total = " + value.get());
System.out.println("one more -> " + addByCas(5));
}
}
Example explained
Line 1value.get() only takes a snapshot; nothing prevents another thread from changing the field before the next line runs.
Line 2compareAndSet(current, next) stores next only if the field still holds current, and returns false when it does not.
Line 3The while (true) loop is the whole idea: a false result means another thread won, so this thread recomputes from the fresh value instead of blocking.
Line 4Because no update is ever silently overwritten, 3 threads times 10000 increments always totals exactly 30000.
Important notes
AtomicInteger does not override equals or hashCode, so two instances holding 5 are not equal; use one as a mutable counter, never as a map key or set element.
Under heavy write contention the CAS retries cost real CPU; LongAdder spreads updates across several cells and scales better, at the price of a sum() that is not an atomic snapshot.
Common mistakes
Writing counter.set(counter.get() + 1) and assuming the atomic type makes it safe: those are two atomic calls with a gap between them, so updates are lost exactly as with count++.
Keeping an AtomicInteger count and an AtomicLong sum for the same statistic: each is atomic alone, so another thread can read a count that already includes an item whose value is not yet in the sum, and the computed average is wrong.
Putting a println or list.add inside the updateAndGet lambda: on contention the function is re-run, so the side effect happens twice while the counter advances once.
Try it yourself
Change, predict, then run
Build an AtomicInteger token wheel whose update rule is updateAndGet(v -> (v + 1) % 5), start three threads that each call it six times, join them, and confirm the printed value is 3 because 18 increments modulo 5 leave 3.
Open the Java workspaceCheck your understanding
A shop keeps AtomicInteger stock and each order thread runs: if (stock.get() > 0) stock.decrementAndGet(); Stock still goes negative under load. Why?
- AtomicInteger guarantees only visibility, not atomicity, so decrementAndGet loses updates
- The field must also be declared volatile before decrementAndGet is atomic
- get() and decrementAndGet() are two separate atomic steps, so another thread can decrement in the gap between the check and the act
- compareAndSet is the only truly atomic method, so decrementAndGet is an ordinary read-modify-write
Show answer
Each call is individually atomic and publishes its result, but the check-then-act pair is not, so two threads can both see stock == 1 and both decrement; the fix is one call, stock.updateAndGet(v -> v > 0 ? v - 1 : v). The first option is tempting because it echoes what volatile cannot do, but an atomic gives both visibility and atomicity for a single operation, so decrementAndGet itself is fine.