Atomic classes
Why count++ loses updates, how AtomicInteger, AtomicLong, AtomicReference and friends fix one variable without a lock, the API you use every week, and the two-variable trap atomics don't cover.
On this page
- Picture it first: a ticket dispenser at the deli
- Quick recap: why count++ loses updates
- How it works: compare, then swap
- Meet the family
- The core API, in one sitting
- The function may run more than once
- The big trap: two atomics are not one atomic
- Counting page views: grow the answer
- Real world
- Advanced: memory modes, intrinsics and edge cases
- How to say it in the interview
🎟️ What are atomic classes?
The atomic classes in java.util.concurrent.atomic (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference and friends) wrap a single variable and give you operations on it that happen all at once or not at all. counter.incrementAndGet() reads, adds one and writes back as one indivisible step, so 32 threads calling it 100,000 times each end at exactly 3,200,000. They do it without a lock: under the hood they use a CPU instruction called compare-and-swap.
For this lesson, we'll start from the bug that makes atomics necessary, meet the family, learn the handful of methods you'll use every week, and then hit the two traps that catch experienced engineers: compound actions across two atomics, and "check, then act" on one. Interviewers like this topic because the first answer ("use AtomicInteger") is easy, and the follow-up ("so is this class thread-safe now?") is where they find out whether you understand what "atomic" actually covers.
Below the line (out of scope)
- Why
count++breaks in the first place: Race conditions - How compare-and-swap works and the ABA problem: Compare-and-swap and the ABA problem
- Hot counters with many writers: LongAdder and striped counters
- Visibility and ordering rules: volatile and Happens-before
The 30-second answer
count++ is three steps (read, add, write), so two threads can interleave and lose updates; volatile doesn't help because it fixes visibility, not atomicity. The atomic classes make one read-modify-write on one variable indivisible, using hardware compare-and-swap instead of a lock, so they never block. get() and set() behave like volatile reads and writes. compareAndSet(expected, new) only writes if nobody changed the value in between; updateAndGet(fn) runs a CAS loop and may call your function more than once, so keep it side-effect free. They don't make several variables change together: for that you need a lock or one AtomicReference to an immutable object. For a write-heavy counter, prefer LongAdder.
What you’ll learn
- Explain why
count++is three steps and whyvolatilegives visibility but not atomicity - Name the atomic family:
AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference, the arrays, the field updaters andVarHandleas the modern foundation - Use the core API correctly:
getAndIncrementvsincrementAndGet,compareAndSet,getAndSet,lazySet/setRelease - Write
updateAndGet/accumulateAndGetfunctions that are side-effect free, because they may be re-run under contention - Describe atomics as lock-free (CAS and fetch-and-add) with volatile memory semantics for
getandset - Spot the bug when correctness depends on two atomics or on check-then-act, and fix it with one
AtomicReferenceto an immutable object or a lock - Choose between
synchronized,AtomicLongandLongAdderfor a counter, with the trade-off in one sentence
Interview radar
Asked very often: Why isn't count++ thread-safe, and how does AtomicInteger fix it?
Core follow-up: incrementAndGet() vs getAndIncrement()? Is AtomicInteger faster than synchronized?
Production angle: Your class uses two AtomicLong fields and the totals still drift. Why?
01Picture it first: a ticket dispenser at the deli
Picture a busy deli counter. Version one: the next number is written on a paper sign. Asha reads "41", Ben reads "41" a moment later, both add one, both write "42" on the sign and both think they're next. One turn just vanished. That's count++ on a plain int: read, add, write, with a gap between each step where someone else can sneak in.
Version two: a red ticket dispenser. You pull, you get a number, and the dispenser advances, all in one motion. Three customers pull and get 42, 43 and 44. Nobody can grab "half a pull". That's an atomic class.
Paper sign vs ticket dispenser
Here's the thing to hold on to: the dispenser protects one number. If the deli also keeps a separate "customers served today" tally on a whiteboard, the dispenser does nothing for that. Keep that in mind; it's the big trap later in the lesson.
02Quick recap: why count++ loses updates
We covered this properly in Race conditions, so here's the short version. count++ compiles to three bytecode-level steps: read the field, add one, write it back. If thread A and thread B both read 5 before either writes, both write 6. Two increments, one recorded.
Marking the field volatile doesn't change that. volatile makes each read and write visible to other threads and stops reordering around it, but read-add-write is still three separate steps. Let's prove it.
Try it yourself first
Before running it, guess: 32 threads each increment three counters 100,000 times. One is a plain int, one is a volatile int, one is an AtomicInteger. Which ones reach 3,200,000?
import java.util.concurrent.atomic.AtomicInteger;
public class CounterRace {
static int plain = 0;
static volatile int vol = 0;
static final AtomicInteger atomic = new AtomicInteger();
public static void main(String[] args) throws Exception {
int threads = 32, perThread = 100_000;
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < perThread; j++) {
plain++; // read, add, write: three steps
vol++; // still three steps, just visible ones
atomic.incrementAndGet(); // one indivisible step
}
});
ts[i].start();
}
for (Thread t : ts) t.join();
int expected = threads * perThread;
System.out.printf("expected %,d%n", expected);
System.out.printf("int %,d (lost %,d)%n", plain, expected - plain);
System.out.printf("volatile int %,d (lost %,d)%n", vol, expected - vol);
System.out.printf("AtomicInteger %,d (lost %,d)%n", atomic.get(), expected - atomic.get());
}
}
Two real runs of java CounterRace.java on JDK 26, Apple M4 Pro (14 cores). The lost counts change every run; the atomic line never does.
expected 3,200,000
int 279,172 (lost 2,920,828)
volatile int 650,280 (lost 2,549,720)
AtomicInteger 3,200,000 (lost 0)
---
expected 3,200,000
int 461,576 (lost 2,738,424)
volatile int 664,863 (lost 2,535,137)
AtomicInteger 3,200,000 (lost 0)
Notice three things:
- The plain
intlost over 85% of the increments. With 32 threads hammering one field, the read-add-write gap is hit constantly. (The JIT may also keepplainin a register for a while, which makes it worse.) volatilelost almost as much. Visibility isn't atomicity. If this surprises you, you're in good company; it's the most common wrong answer to this question.AtomicIntegerwas exact, every run, with no lock anywhere in the code.
03How it works: compare, then swap
So how does AtomicInteger add one without a lock? The core idea is compare-and-swap (CAS): one CPU instruction that says "if this memory location still holds 5, put 6 there; otherwise, tell me I lost". It's like the deli clerk saying "I'll serve number 42, but only if nobody's already called 42".
If you lose, nothing is written. You re-read the new value and try again. Nobody ever waits holding a lock, which is why we call these classes lock-free: some thread always makes progress. Here are both versions side by side.
count++ loses an update; compare-and-set retries instead
The right side is the mental model, and it's exactly how updateAndGet works. For plain counting, HotSpot goes one better: incrementAndGet and getAndAdd are JIT intrinsics that become a single hardware fetch-and-add (LOCK XADD on x86, LDADDAL on ARMv8.1+), so there isn't even a retry loop. CAS itself, the ABA problem and the retry cost get their own lesson: Compare-and-swap and the ABA problem.
04Meet the family
The package is small. Here's what's in it and when you'd reach for each.
| Class | Wraps | Typical use |
|---|---|---|
AtomicInteger, AtomicLong |
an int / long |
counters, sequence numbers, IDs |
AtomicBoolean |
a boolean |
"run once" flags: if (started.compareAndSet(false, true)) |
AtomicReference<V> |
an object reference | swap a whole immutable config or state object at once |
AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray |
an array of slots | per-bucket counters; each slot is atomic, the array as a whole is not |
AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater |
a volatile field in your own class |
atomics without one extra object per instance (saves memory when you have millions) |
AtomicStampedReference, AtomicMarkableReference |
a reference + int stamp / boolean mark | defending against ABA |
LongAdder, LongAccumulator, DoubleAdder, DoubleAccumulator |
a striped sum | hot counters with many writers (Java 8+) |
Underneath, since Java 9, the public foundation is VarHandle (java.lang.invoke). A VarHandle is a typed handle to a field or array slot that lets you do the same atomic operations (compareAndSet, getAndAdd, getAcquire, setRelease...) on any field, without a wrapper object. It replaced the internal sun.misc.Unsafe tricks library authors used to rely on (JEP 193). The atomic classes' Javadoc now defines every method's memory effect in VarHandle terms; AtomicReference and the arrays use VarHandles directly, while AtomicInteger and AtomicLong still call the JDK-internal Unsafe for bootstrapping reasons. You'll rarely need VarHandle in application code, but it's good to know they're two faces of the same machinery.
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
class Hits {
private volatile long count; // a plain field, no AtomicLong object
private static final VarHandle COUNT;
static {
try { COUNT = MethodHandles.lookup().findVarHandle(Hits.class, "count", long.class); }
catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
long hit() { return (long) COUNT.getAndAdd(this, 1L) + 1; } // same effect as incrementAndGet()
}
05The core API, in one sitting
You'll use about eight methods. Here's the one rule that makes the names easy: the word order tells you which value comes back. getAndIncrement gets, then increments, so it returns the old value (like i++). incrementAndGet increments, then gets, so it returns the new value (like ++i).
Try it yourself first
Start at 41. Predict each line: getAndIncrement(), incrementAndGet(), compareAndSet(43, 100), compareAndSet(43, 200), updateAndGet(x -> x * 2), accumulateAndGet(5, Math::max), getAndSet(0).
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTour {
public static void main(String[] args) {
AtomicInteger ticket = new AtomicInteger(41);
System.out.println("getAndIncrement() returned " + ticket.getAndIncrement() + ", now " + ticket.get());
System.out.println("incrementAndGet() returned " + ticket.incrementAndGet() + ", now " + ticket.get());
boolean ok = ticket.compareAndSet(43, 100); // expect 43: it is 43, so swap
System.out.println("compareAndSet(43, 100) -> " + ok + ", now " + ticket.get());
ok = ticket.compareAndSet(43, 200); // expect 43: it is 100, so refuse
System.out.println("compareAndSet(43, 200) -> " + ok + ", now " + ticket.get());
System.out.println("updateAndGet(x -> x * 2) -> " + ticket.updateAndGet(x -> x * 2));
System.out.println("accumulateAndGet(5, Math::max) -> " + ticket.accumulateAndGet(5, Math::max));
System.out.println("getAndSet(0) returned " + ticket.getAndSet(0) + ", now " + ticket.get());
}
}
getAndIncrement() returned 41, now 42
incrementAndGet() returned 43, now 43
compareAndSet(43, 100) -> true, now 100
compareAndSet(43, 200) -> false, now 100
updateAndGet(x -> x * 2) -> 200
accumulateAndGet(5, Math::max) -> 200
getAndSet(0) returned 200, now 0
Here's the whole API on one card:
| Method | What it does | Memory effect |
|---|---|---|
get() / set(v) |
read / write | like a volatile read / write |
lazySet(v) = setRelease(v) |
write that may become visible a little later | release only (cheaper, Advanced) |
getAndIncrement() / incrementAndGet() |
add one, return old / new | volatile read + write |
getAndAdd(d) / addAndGet(d) |
add d, return old / new |
volatile read + write |
getAndSet(v) |
swap, return old | volatile read + write |
compareAndSet(expect, v) |
write v only if the value is expect; returns true/false |
volatile read + write |
updateAndGet(fn) / getAndUpdate(fn) |
apply fn in a CAS loop |
volatile read + write |
accumulateAndGet(x, fn) / getAndAccumulate(x, fn) |
apply fn(current, x) in a CAS loop |
volatile read + write |
That "like a volatile" column matters. An AtomicReference.set(config) publishes the object: a thread that later get()s it sees the fully built config, for the same happens-before reasons as a volatile write.
06The function may run more than once
updateAndGet(fn) is a loop: read the current value, call fn, try compareAndSet(old, fn(old)), and if another thread got there first, call fn again. The Javadoc says it directly: the function "should be side-effect-free, since it may be re-applied when attempted updates fail due to contention among threads".
Let's count how often that happens. This program deliberately breaks the rule by counting calls inside the lambda.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class RetryCount {
public static void main(String[] args) throws Exception {
AtomicLong total = new AtomicLong();
AtomicInteger calls = new AtomicInteger(); // how many times our lambda ran
int threads = 8, perThread = 100_000;
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) {
ts[i] = new Thread(() -> {
for (int j = 0; j < perThread; j++) {
total.updateAndGet(x -> {
calls.incrementAndGet(); // a side effect: exactly what you should NOT do
return x + 1;
});
}
});
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.printf("updates applied %,d%n", total.get());
System.out.printf("lambda calls %,d (%,d re-runs)%n", calls.get(), calls.get() - total.get());
}
}
updates applied 800,000
lambda calls 3,753,540 (2,953,540 re-runs)
800,000 updates, almost 3.8 million lambda calls on this run (another run gave 4.2 million). The sum is still exact, which is the point of CAS. But if that lambda had sent an email, logged an audit line or charged a card, it would have done so almost five times per update. Keep the function pure: compute a new value from the old one, nothing else.
07The big trap: two atomics are not one atomic
This is the bit that trips up people who already "know" atomics. Each atomic makes one variable safe. The moment correctness depends on two variables agreeing, you're back to a race, even if both are atomic.
Here's a real bug shape. Two wallets, Alice and Bob, start with 500 each. A mover thread shifts 10 back and forth, so the total should always be 1,000. An auditor thread keeps checking.
Two atomics do not make an atomic pair
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class TwoAtomics {
// Two wallets. Money only moves between them, so the total must always be 1,000.
static final AtomicLong alice = new AtomicLong(500);
static final AtomicLong bob = new AtomicLong(500);
record Wallets(long alice, long bob) { }
static final AtomicReference<Wallets> both = new AtomicReference<>(new Wallets(500, 500));
public static void main(String[] args) throws Exception {
long deadline = System.nanoTime() + 1_000_000_000L; // run for 1 second
Thread mover = new Thread(() -> {
while (System.nanoTime() < deadline) {
alice.addAndGet(-10); bob.addAndGet(10); // each step atomic, the pair is not
bob.addAndGet(-10); alice.addAndGet(10);
both.updateAndGet(w -> new Wallets(w.alice() - 10, w.bob() + 10));
both.updateAndGet(w -> new Wallets(w.alice() + 10, w.bob() - 10));
}
});
mover.start();
long checks = 0, badTwo = 0, badOne = 0, example = 1_000;
while (mover.isAlive()) {
checks++;
long total = alice.get() + bob.get();
if (total != 1_000) { badTwo++; example = total; }
Wallets w = both.get();
if (w.alice() + w.bob() != 1_000) badOne++;
}
System.out.printf("audits run %,d%n", checks);
System.out.printf("two AtomicLongs, wrong %,d (e.g. total = %d)%n", badTwo, example);
System.out.printf("one AtomicReference, wrong %,d%n", badOne);
}
}
audits run 153,978,859
two AtomicLongs, wrong 5,129,948 (e.g. total = 990)
one AtomicReference, wrong 0
Over 5 million bad audits in one second (other runs: 6.5 and 7.0 million, sometimes showing 1,010). Notice the fix: we didn't add a lock. We made the two numbers one value, an immutable Wallets record, and swapped the whole thing with one CAS. That's the go-to pattern when the state is small. When it isn't, or when you also need to wait for a condition, use synchronized (see synchronized and monitors).
The same trap hides inside a single atomic too: check-then-act. if (sold.get() < 100) sold.incrementAndGet(); reads, decides, then writes, as two separate atomic calls. With 32 threads released at once through a CountDownLatch, three of my five runs sold 101 seats out of 100. Rewriting it as sold.getAndUpdate(n -> n < 100 ? n + 1 : n) puts the check inside the CAS, and every run sold exactly 100.
08Counting page views: grow the answer
Let's solve a real one the way you'd grow into it in an interview: 32 request threads each record page views, and a dashboard reads the total every few seconds.
Naive approach: A shared int (or volatile int) with count++
It's one line and it looks fine in a unit test with one thread. Some people add volatile "for thread safety".
Challenges: read-add-write races. In the run above, the int lost 2.9 million of 3.2 million increments and the volatile int lost 2.5 million. The dashboard is confidently wrong.
Solid approach: AtomicLong.incrementAndGet()
Every view counts, there's no lock to deadlock on, and reads are a cheap volatile get(). This is the right default for most counters, IDs and sequence numbers.
Challenges: all 32 threads fight over one memory location. Each increment needs exclusive ownership of that cache line, so under heavy write contention threads queue up in hardware. In my quick run (32 threads x 1M increments, rough System.nanoTime timing, second round after warm-up) AtomicLong took 0.8 to 1.4 s, about the same as a synchronized counter.
Staff approach: LongAdder when writes dominate reads
LongAdder gives each contending thread its own cell to add into and only adds the cells up when you call sum(). Same run: about 20 ms, roughly 40 to 75 times faster. It's the right tool when you write a lot and read rarely (metrics, stats, rate counters).
When not to: sum() is not an atomic snapshot under concurrent updates, it uses more memory per counter, and it has no compareAndSet. If you need "increment and give me the exact new value" (ticket numbers, IDs), stay with AtomicLong. Details in LongAdder and striped counters.
Counting page views from 32 threads: three answers
The harness behind those numbers is small: start 32 threads, each calls one of these a million times, join, time it, and repeat once so the JIT has warmed up. Timings vary a lot by CPU and JVM; treat them as a shape, not a benchmark (use JMH for real numbers).
Runnable viaLock = () -> { synchronized (lock) { syncCount++; } };
Runnable viaAtomic = atomic::incrementAndGet; // AtomicLong
Runnable viaAdder = adder::increment; // LongAdder
One run, second round. Across three runs AtomicLong ranged from 766 to 1,446 ms and LongAdder from 17 to 21 ms.
synchronized 32,000,000 views in 924 ms
AtomicLong 32,000,000 views in 766 ms
LongAdder 32,000,000 views in 21 ms
09Real world
- Netty keeps a reference count on every pooled
ByteBuf. With millions of buffers alive, one extraAtomicIntegerobject per buffer adds up, so Netty uses anAtomicIntegerFieldUpdateron a plainvolatile intfield instead (AbstractReferenceCountedByteBufandReferenceCountUpdater). - Dropwizard Metrics
CounterandMeterare backed byLongAdder: request threads write constantly, the reporter reads every few seconds. Exactly the STAFF case above. ConcurrentHashMap.size()uses the same striped-cell idea asLongAdderinternally (CounterCell[]), which is whysize()is an estimate while other threads are writing.
10Advanced: memory modes, intrinsics and edge cases
Memory modes. Since Java 9 every atomic class exposes the VarHandle access modes: plain (getPlain/setPlain), opaque, acquire/release (getAcquire/setRelease) and volatile (get/set). lazySet is the old name for setRelease: writes before it can't move after it, but a later volatile read in the same thread may be reordered before it, so it skips the expensive store-load fence on x86. It's useful for "I'm the only writer and readers can see it a few nanoseconds late", like clearing a slot in a single-producer queue. Doug Lea's "Using JDK 9 Memory Order Modes" is the reference.
Weak CAS. weakCompareAndSetPlain / weakCompareAndSetVolatile may fail spuriously (return false even when the value matched). On ARM, which implements CAS with load-linked / store-conditional pairs, that can be cheaper inside a retry loop. The old weakCompareAndSet was deprecated in Java 9 because its name hid that it had plain memory semantics.
Hardware. On x86, compareAndSet becomes LOCK CMPXCHG and getAndAdd becomes LOCK XADD, both full fences. On ARMv8.1+ with LSE (my M4 reports UseLSE = true), HotSpot emits CASAL and LDADDAL; older ARM uses LDAXR/STLXR loops. "Lock-free" means no Java lock and no blocking. It does not mean free: a contended atomic still costs a cache-line transfer between cores on every write.
False sharing. Two hot AtomicLongs allocated next to each other can share a 64-byte cache line, so threads updating different counters still slow each other down. LongAdder's cells are padded with @jdk.internal.vm.annotation.Contended (JEP 142) for exactly this reason.
Many candidates are quick to swap every int for an AtomicInteger and declare victory. That is frequently a mistake, because thread safety is about invariants, not fields. if (x.get() == 0) x.set(1), lower.get() <= upper.get(), or "debit one, credit the other" are all multi-step and still race. Ask: does correctness depend on more than one read or write lining up? If yes, use one CAS on an immutable object, or a lock.
An uncontended incrementAndGet costs a few nanoseconds (roughly 5 to 20 ns depending on CPU), close to an uncontended synchronized. Under heavy contention both degrade toward the cost of moving one cache line between cores, and in my 32-thread run they landed within 2x of each other. LongAdder stayed around 20 ms for 32 million increments. updateAndGet under contention called its function 4 to 5 times per successful update in the retry demo above.
| Need | Pick | Why |
|---|---|---|
| One counter, exact value needed on each increment (IDs, tickets) | AtomicLong |
returns the new value atomically |
| One counter, many writers, read occasionally (metrics) | LongAdder |
writers rarely collide; sum() is approximate under writes |
| A "do this once" flag | AtomicBoolean.compareAndSet(false, true) |
exactly one thread wins |
| Swap a whole config or state | AtomicReference to an immutable object |
readers see old or new, never half |
| Several fields that must change together, or waiting for a condition | synchronized / a lock |
atomics can't group variables or block |
| Millions of objects each needing an atomic field | field updater or VarHandle |
no wrapper object per instance |
The atomic toolbox on one board
11How to say it in the interview
When I ask about atomics, I'm listening for three things. First, can you say why count++ fails and why volatile alone doesn't fix it? Second, do you know atomics cover one variable, and can you spot the multi-variable or check-then-act bug? Third, do you know the trade-off between AtomicLong, LongAdder and a lock, with a reason rather than "atomics are faster"?
Talk track
"count++ is read, add, write. Two threads can both read 5 and both write 6, so we lose an update; volatile makes the writes visible but doesn't make the three steps one. AtomicInteger.incrementAndGet() does it as one indivisible operation using the CPU's compare-and-swap or fetch-and-add, so there's no lock and no blocking. get and set have volatile semantics, so an AtomicReference also safely publishes objects. For updates like 'double it' I'd use updateAndGet, keeping the lambda pure because it can be retried. The catch is that atomics protect one variable: if two values must stay consistent, I either put them in one immutable object behind an AtomicReference or use a lock. And for a counter with heavy writes and rare reads, I'd pick LongAdder over AtomicLong."
Whiteboard summary
count++= read + add + write;volatilegives visibility, not atomicity- Atomics = one variable, one indivisible op, lock-free via CAS / fetch-and-add
getAndXreturns old,xAndGetreturns newcompareAndSet(expect, new)returns false if someone moved firstupdateAndGet(fn): fn may re-run, keep it side-effect freeget/set= volatile semantics;lazySet=setRelease- Two atomics != atomic pair: one immutable object +
AtomicReference, or a lock - Many writers, rare reads:
LongAdder
Further reading (14)
- java.util.concurrent.atomic package summary (Java SE 21): the family, memory effects of get/set/lazySet/compareAndSet, field updaters, arrays
- AtomicInteger (Java SE 21 API): each method's memory effect defined via VarHandle; updateAndGet function must be side-effect-free; weakCompareAndSet deprecated since 9
- VarHandle (Java SE 21 API): access modes: plain, opaque, acquire/release, volatile
- JEP 193: Variable Handles: why VarHandle replaced sun.misc.Unsafe for atomic field access (Java 9)
- LongAdder (Java SE 21 API): preferable to AtomicLong under high contention for statistics; sum() is not an atomic snapshot
- OpenJDK jdk21u source: AtomicInteger.java: implemented on jdk.internal.misc.Unsafe; getAndAdd is a HotSpot intrinsic
- Doug Lea: Using JDK 9 Memory Order Modes: plain, opaque, release/acquire and volatile modes; what lazySet/setRelease really guarantees
- Java Language Specification (Java SE 21) Chapter 17: §17.4 happens-before: why atomic get/set publish like volatile
- Aleksey Shipilev: Java Memory Model Pragmatics: atomicity vs visibility, and how HotSpot maps them to hardware
- JEP 142: Reduce Cache Contention on Specified Fields: @Contended padding used by LongAdder cells against false sharing
- Netty: AbstractReferenceCountedByteBuf: refCnt is a volatile int updated through AtomicIntegerFieldUpdater, avoiding one AtomicInteger per buffer
- Netty: ReferenceCountUpdater: the shared CAS logic behind Netty reference counting
- Dropwizard Metrics: Counter.java: production counters backed by LongAdder (Meter too)
- Java Concurrency in Practice (Goetz et al., 2006): §2.3 atomicity and compound actions, §4.1 invariants across variables, ch. 15 atomic variables and nonblocking synchronization