The Last Copy of the Book
In Chapter 20 we entrusted the same counter to two threads. We saw that value++ is not a single action: it reads a number, calculates another and writes it. When two threads go through those steps together, one increment can be lost. A common monitor, used through synchronized, solved the problem. We also encountered AtomicInteger, which can increment a shared value without making us write that monitor. Why, then, devote another chapter to atomics? Because incrementing is the easy case. In applications we often need to make a decision based on the value read.
Imagine a library with five reservable copies of a book. Twenty requests arrive almost simultaneously. A reservation can succeed only if at least one copy remains; after five successes, all the others must be rejected. The condition to preserve is simple to state: the number of available copies must never become negative, and accepted requests must not exceed five. This condition is an invariant: it must remain true in every state other threads can observe.
The code if (availableCopies > 0) availableCopies--; already seems to describe the rule. In reality a thread can read 1, stop, let another also read 1 and only then continue. Both have decided to accept the last copy. Even declaring the field volatile does not turn checking and decrementing into an indivisible decision: volatile concerns visibility and ordering of accesses, rather than automatic composition of several statements. A synchronized block enclosing both the check and the modification would be correct; this time we want to understand whether we can express the same decision for a single variable with an atomic operation.
Definition – Atomic Operation. For accesses following the same protocol, an atomic operation appears as one indivisible step on the state involved. An atomic increment prevents another increment from interleaving between its read and write. CAS adds a condition: it replaces the value only if it matches the expected value; otherwise it reports the failed update. Not every atomic operation requires an expected value from the caller. The guarantee covers only the state of the operation: it does not also make the application request, printing, or writing to disk indivisible. We must therefore state which values need to change together.
The Library Is Already in the JDK
Java offers java.util.concurrent.atomic in the standard module java.base. AtomicInteger holds an int; AtomicLong holds a long; AtomicBoolean a boolean; AtomicReference<T> a reference to an object. These classes are not generic substitutes for ordinary Integer, Long or Boolean: they are mutable objects designed to let threads sharing state communicate. In the chapter's core we use only stable Java 25 APIs, without external dependencies or preview options.
Let us return to the atomic counter from Chapter 20. The method incrementAndGet() updates the counter and returns the new value; getAndIncrement() updates the same counter but returns the previous one. If we start from 7, the first call to getAndIncrement() returns 7 and leaves 8; a subsequent incrementAndGet() returns 9 and leaves 9. This distinction is important when the result becomes a ticket number or position: an off-by-one error can assign the same number twice or skip one.
An independent counter is a good problem for AtomicInteger. However, if the number governs a decision, reading with get() and modifying in a separate call is not enough. Figure 20A.1 compares the two sequences when only one copy remains.
Comparing and Setting
compareAndSet is often abbreviated as CAS, from compare and set. It receives the value we expect and the one we would like to install. If the current value matches the expected one, it replaces it and returns true. Otherwise it leaves the state unchanged and returns false. The comparison in our AtomicInteger concerns two int values; later we will see that for AtomicReference it concerns reference identity.
Suppose A and B both read 1. A attempts compareAndSet(1, 0) and succeeds. B attempts the same replacement, but finds 0: it receives false. B should not conclude that there has been a library error; it must reread the state. On the new read it finds zero and rejects the reservation. A CAS failure is a normal possibility in a concurrent program. This brief repetition forms a CAS loop: observe, propose a new value, attempt, retry if someone has changed it.
The complete program AtomicReservations applies the rule. Compile it from directory examples/C20A with javac --release 25 AtomicReservations.java and run it with java AtomicReservations. It uses virtual threads, already explained in C20, to start twenty requests. Choosing the thread type does not make the counter atomic: correctness comes from the update protocol. Two CountDownLatch objects organize the test: each worker signals on ready that it has reached the gate, then waits on go.await(). The main thread waits for the twenty signals and opens the gate with countDown(). This does not guarantee arrival order, but makes overlap of requests plausible without resorting to sleep.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicReservations {
private final AtomicInteger availableCopies;
public AtomicReservations(int initialCopies) {
if (initialCopies < 0) {
throw new IllegalArgumentException("Copies cannot be negative");
}
availableCopies = new AtomicInteger(initialCopies);
}
public boolean reserve() {
while (true) {
int observed = availableCopies.get();
if (observed == 0) {
return false;
}
if (availableCopies.compareAndSet(observed, observed - 1)) {
return true;
}
}
}
public int available() {
return availableCopies.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicReservations catalog = new AtomicReservations(5);
AtomicInteger accepted = new AtomicInteger();
CountDownLatch ready = new CountDownLatch(20);
CountDownLatch go = new CountDownLatch(1);
List<Thread> readers = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Thread reader = Thread.ofVirtual().start(() -> {
try {
ready.countDown();
go.await();
if (catalog.reserve()) {
accepted.incrementAndGet();
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
});
readers.add(reader);
}
ready.await();
go.countDown();
for (Thread reader : readers) {
reader.join();
}
if (accepted.get() != 5 || catalog.available() != 0) {
throw new AssertionError("Copy invariant violated");
}
System.out.println("Accepted reservations: " + accepted.get());
System.out.println("Available copies: " + catalog.available());
}
}
Let us pause on the method reserve(). get() reads the current number. If it is zero, the method answers false and does not attempt subtraction. Otherwise it calculates observed - 1, but that calculation is only a proposal. The point at which the reservation succeeds is compareAndSet: the proposal is installed only if nobody else has changed the number read. When CAS fails, the while restarts from the beginning; it does not reuse the old number, because the data justifying the decision is no longer current. The constructor rejects a negative initial number, so the invariant starts out true. A real application would also define how to add new copies, but every method modifying this same number would have to respect the same protocol.
In main, twenty workers call reserve(). A second AtomicInteger counts how many received true; it is only a test counter, rather than part of the reservation. After every join(), the main thread checks accepted == 5 and available == 0. The final output is therefore stable: Accepted reservations: 5 and Available copies: 0. We do not know which five readers obtained a copy, nor can we deduce it from the program. Repeating execution helps find implementation errors, whereas the reason why the limit is respected lies in CAS and in the fact that every success subtracts exactly one from a positive value.
Key concept – Where the Decision Falls. An isolated
get()reserves nothing. A successfulcompareAndSetis the point at which the change becomes effective for that atomic state. Identifying this point makes reasoning possible even when different threads' code lines interleave unpredictably.
The Loop Has a Limit of Applicability
A CAS loop does not put the thread into waiting for a future event. If all copies are gone, our method exits; it does not spin waiting for someone to return one. If, instead, several threads continuously modify the counter, a caller may have to retry many times. We do not promise that every individual attempt completes within a fixed time or that CAS is faster than a monitor on any machine. When the problem is to wait for a returned copy, a waiting and notification protocol is needed, or a structure such as BlockingQueue or a Semaphore, according to what we represent. Replacing waiting with a while repeatedly calling get() wastes CPU and does not define the allocation policy.
For a single value, AtomicInteger makes the update boundary very precise. This does not mean that every if followed by an increment can be mechanically transformed into CAS. We must check that the new value depends only on the state read and that the attempt can be repeated without producing duplicate external effects. A reservation that charges a card or sends a confirmation requires a transactional boundary and a failure policy; the counter's CAS does not supply them.
Update Functions Can Be Called Several Times
The library offers getAndUpdate and updateAndGet when we want to transform a value without explicitly writing the loop. The former returns the previous value, and the latter the new one. To both we pass a function that receives the current value and calculates the next candidate. The function can be applied several times if another thread intervenes between calculation and update: the documentation therefore requires it to be free of side effects.
In the PureUpdate program, we increase the number of seats by two without exceeding the limit ten. Math.min(10, value + 2) depends only on the received parameter; if the API recalculates it, it sends no messages and modifies no other objects. The example's small values avoid arithmetic overflow; an unbounded counter must instead decide what to do on reaching Integer.MAX_VALUE.
import java.util.concurrent.atomic.AtomicInteger;
public class PureUpdate {
public static void main(String[] args) {
AtomicInteger seats = new AtomicInteger(7);
int previous = seats.getAndUpdate(value -> Math.min(10, value + 2));
System.out.println("Before: " + previous + ", after: " + seats.get());
int next = seats.updateAndGet(value -> Math.min(10, value + 2));
System.out.println("Returned value: " + next);
}
}
The program prints Before: 7, after: 9, then Returned value: 10. The method name tells us which snapshot we receive as the result, while the stored value changes atomically. Now consider a function that calls sendEmail() before returning the new number. If the update attempt must restart, the email could be sent twice for a single successful modification. A function recording a payment presents the same error, with more serious consequences. To separate calculation from the effect, we first complete the decision on state and then manage communication with a suitable protocol, capable of also handling a failure between the two steps.
Good practice – Purity in the Attempt. In a CAS loop and in functions passed to
getAndUpdate,updateAndGet,getAndAccumulateor similar methods, calculation of the candidate must be repeatable. A pure function returns the same result for the same arguments and does not modify state observable outside that calculation. It is not enough for the final numerical result to be right: the effects along the path also count.
A Sequence Number Is Another Question
If we must assign a sequence identifier in memory, AtomicLong.getAndIncrement() can return a distinct value to every caller as long as we remain in the agreed numerical range. Here there is no need to read first, check an available copy and then decrement: the single operation already expresses the assignment. But if the identifier must remain unique across several processes or survive restarts, an AtomicLong in one JVM is not enough. The archive storing the data must participate in the guarantee. The same name “counter” hides two different problems: assigning a local number and coordinating a persistent identity.
AtomicBoolean should also be read this way. It can represent a transition false → true attempted by many threads, for example when an initialization action must be claimed only once. compareAndSet(false, true) says who obtained the right to try. It does not say that initialization has already finished, or succeeded: if the other threads need the result, they must be able to wait and observe success or error with a richer protocol, such as a Future. Using a boolean called ready immediately after setting it to true could publish a premature promise.
Let Us Change the Question: How Many Views Have We Had?
The library wants to know how many times a page has been visited. Every request adds one. The total will be shown in a report after activities finish, or read during work as an approximate indication. Here we are not deciding whether to assign the last copy based on an exact snapshot. We already know how to use AtomicLong for a correct count, but if many threads update the same point in memory, they can contend for that single value. The problem changes again: the question is whether a specialized structure can distribute updates better.
LongAdder maintains a total through several internal components and combines them when we request sum(). Under strong contention it can offer better throughput than AtomicLong, at the cost of more memory. The word can matters: with few threads, few modifications or a particular platform, the gain may be zero or negative. Figure 20A.2 helps us see the trade-off: a single value facilitates a precise read; several contributions reduce concentration of updates but require a sum.
sum() during updates is not an atomic snapshot of the total.In the AccessStatistics program, eight workers each increment one thousand times. The main thread calls sum() after the eight join calls, when updates are complete. This is why the expected total is exactly 8,000. If instead another thread called sum() while the workers were still updating, the number would be useful for monitoring a trend, but would not be an atomic snapshot to use for assigning the last copy.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.LongAdder;
public class AccessStatistics {
public static void main(String[] args) throws InterruptedException {
LongAdder views = new LongAdder();
List<Thread> workers = new ArrayList<>();
for (int i = 0; i < 8; i++) {
Thread worker = Thread.ofVirtual().start(() -> {
for (int j = 0; j < 1_000; j++) {
views.increment();
}
});
workers.add(worker);
}
for (Thread worker : workers) {
worker.join();
}
long total = views.sum();
if (total != 8_000) {
throw new AssertionError("Lost views: " + total);
}
System.out.println("Completed views: " + total);
}
}
DoubleAdder offers a similar idea for double values; we must remember that floating-point calculations already have their own rounding rules and that a different order of addition can produce small differences. LongAccumulator and DoubleAccumulator allow an accumulation function and identity element to be defined: for example we might want to observe the maximum of a series of durations. The function must be suitable for combining independent contributions; an arbitrary subtraction is not, because changing order and grouping changes the result. These APIs deserve space when the requirement is really statistical and a comparison on the real workload shows a benefit. We do not use LongAdder for a serial number that every caller must receive uniquely: sum() does not offer that precise reservation.
Further reading – Measuring Without Telling Ourselves a Story. A single
System.nanoTime()around a loop inmaindoes not demonstrate that a class is generally faster. The workload must declare how many threads write, how often reading occurs, the ratio of useful work to updating, and which JDK and machine are measured. JVM warm-up and variation between executions must be considered. First we verify the invariant; then we measure throughput, meaning the number of operations completed in an interval, and the time of operations when this interests the user.
Two Fields That Must Remain in Agreement
So far the important state lived in a single int. Now suppose a screen shows “available copies” and “reserved copies” together. In every coherent snapshot their sum must be five. Two separate AtomicInteger objects would prevent lost updates on individual fields, but would not make the pair atomic: a reader could observe the first after decrementing and the second before incrementing. It would see a sum of four, representing no complete reservation state.
A first solution is to use a common lock to update and read both fields together. When the state is small, immutable and entirely in memory, we can also create a new snapshot with both values and publish it by replacing a single reference. AtomicReference<State> does not magically make the object's fields atomic: it makes replacement of the reference atomic. It works here because each State does not change after construction and all readers observe the pair by taking a single reference. The program's record checks the invariant in its constructor, so an incomplete snapshot cannot be constructed with those two values.
Figure 20A.3 shows the difference. With two independent containers the reader can cross an intermediate phase; with a reference to the immutable snapshot it sees “5, 0” or “4, 1”. Replacement is a single publication point.
The AtomicImmutableState program uses twenty requests for the five copies. It does not store readers' names: it develops only the invariant linking the two numbers. Here too the method creates a new state from the observed one and attempts compareAndSet. If the reference has changed, it rereads both numbers from the new snapshot and rebuilds the candidate. After the join calls, the program prints Available: 0, reserved: 5.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicImmutableState {
record State(int available, int reserved) {
State {
if (available < 0 || reserved < 0 || available + reserved != 5) {
throw new IllegalArgumentException("Invalid state");
}
}
}
private final AtomicReference<State> state =
new AtomicReference<>(new State(5, 0));
public boolean reserve() {
while (true) {
State before = state.get();
if (before.available() == 0) {
return false;
}
State after = new State(before.available() - 1, before.reserved() + 1);
if (state.compareAndSet(before, after)) {
return true;
}
}
}
public State snapshot() {
return state.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicImmutableState catalog = new AtomicImmutableState();
List<Thread> readers = new ArrayList<>();
for (int i = 0; i < 20; i++) {
readers.add(Thread.ofVirtual().start(catalog::reserve));
}
for (Thread reader : readers) {
reader.join();
}
State result = catalog.snapshot();
if (result.available() != 0 || result.reserved() != 5) {
throw new AssertionError("State invariant violated");
}
System.out.println("Available: " + result.available()
+ ", reserved: " + result.reserved());
}
}
For references, compareAndSet(before, after) compares the current reference with before by identity, rather than calling equals() to compare the contents. Our code retries on the reference just read, so the distinction does not break the example; however, it is important when distinct objects represent equal values. Immutability must be real: a record containing a mutable List does not automatically become immutable. We would need to make a defensive copy and prevent subsequent changes to relevant elements, or use a different state model.
Warning – Publishing State Does Not Complete a Transaction. If after CAS we must write a file, update a database or send a confirmation, an error can occur between the two steps. The memory snapshot remains coherent, but does not necessarily represent what was saved. A database shared by several processes requires a transaction or another form of coordination in the archive. A lock in a single JVM and
AtomicReferencedo not extend their guarantee beyond that process's memory.
The History the Final Value Does Not Tell: ABA
Imagine that a thread reads reference A and prepares a modification. Meanwhile another thread replaces A with B and then puts back the same object A. The first thread still finds A and its CAS can succeed. For a simple rule “is the current value A?” this can be fine. For a data structure where it matters that nobody has touched the node in the meantime, it is a problem: the history A → B → A is not equivalent to “nothing happened”. This is called the ABA problem.
AtomicStampedReference maintains a reference and version number that can be updated together. The thread can then check not only that the reference is A, but also that the version is still the one observed. This does not turn the version number into a universal solution: its increment and possible return after overflow must be designed. In the main path we do not need a lock-free data structure; the box serves to avoid promising that CAS alone distinguishes every possible history.
Which Tool Do I Choose?
A reliable choice begins with the required property, rather than the list of available classes. If a library must prevent the sixth reservation, an exact decision on shared state is needed. If it only needs to show a frequently updated view counter, an approximate reading during work can be acceptable. If it must confirm a persistent reservation together with the reader's name, the rule involves several pieces of data and perhaps several processes. The same statement increment() does not make these three problems equivalent.
| Required property | Tool to consider | Boundary to verify |
|---|---|---|
| Update a single value with an exact decision | AtomicInteger, AtomicLong or AtomicBoolean |
The entire decision lies in the atomic operation or CAS loop. |
| Publish an immutable snapshot of several values in memory | AtomicReference<State> |
All readers use the same snapshot; the objects do not change after publication. |
| Count many events for a statistic | LongAdder or DoubleAdder |
A concurrent reading must not serve as an atomic snapshot; the benefit must be measured. |
| Protect a rule across several mutable objects | synchronized or an explicit lock |
Every relevant access uses the same protocol and the critical section has the right boundary. |
| Wait for availability or limit accesses | BlockingQueue, Semaphore or equivalent coordination |
When to wait, wake and handle interruption or closure is defined. |
| Save together data that must survive or coordinate several processes | Archive transaction | The guarantee covers errors, restarts and other processes according to the archive's contract. |
This table is not a contest in which the tool with the newest name always wins. A short lock can be the clearest solution for a rule crossing several fields. An AtomicInteger can precisely express a single variable. An adder can help a heavily contended counter, but its distributed structure changes precisely what we can ask of sum() while updates arrive. Under strong contention, even a CAS loop can retry many times and consume CPU: measuring and understanding the workload is part of the choice.
In Chapter 25 we encounter Files.move with ATOMIC_MOVE. The adjective is the same, but concerns another boundary: publication of a file to filesystem observers, within the provider's limits. It does not mean that an AtomicInteger makes the file atomic, or that an atomic move alone guarantees data durability after a power-off. In C27, an HTTP client can retry a request after a timeout; local CAS does not automatically make that remote request idempotent. To recognize the right problem, we must always ask: who observes the state, where does it live and which set of changes must appear indivisible?
Tools Closer to the Hardware: Further Reading
The package also contains atomic arrays, useful when every position has its own atomic update, and updaters for particular volatile fields. AtomicMarkableReference associates a boolean mark with a reference; AtomicStampedReference, already encountered, associates an integer version. VarHandle, in package java.lang.invoke, permits operations on variables with different access modes and memory ordering. These tools are useful when designing concurrent structures or specialized interoperability; however, they increase the number of contracts to understand. The basic path stays with ordinary operations of atomic classes and locks, because they already allow us to solve the problems we have constructed. Optimizing memory order before proving the need does not make a program more understandable.
Java version – Stable APIs. The atomic toolkit has been in the platform since long before Java 25; the classes used in the chapter's programs belong to
java.baseand do not require--enable-preview.VarHandleis also a stable API, but we present it as further reading because using it correctly requires a more detailed model of memory effects.
Let Us Try to Transfer the Brick
We start from the five copies and change one condition at a time. If every reader must receive their reservation number, the boolean returned by reserve() is not enough: we must decide whether the number identifies a position among the five copies, a temporal order or a persistent record. In the first case we can have the successful CAS return the number associated with the previous state, documenting whether counting starts at zero or one. In the second case we must also define the desired order and what happens after a restart. The reader has understood the model when they can formulate these questions before changing the method's return type.
If instead we turn reservations into views of the book entry, the limit of five no longer exists and we do not need to reject the sixth reader. Considering LongAdder becomes reasonable: many requests add one and a periodic report reads the total. The answer changes because the invariant changes, rather than because we have learned new syntax. Finally, if we want to save the reader's name, assigned copy and confirmation in an archive, we return to asking which system can make the persistent whole coherent. No method of AtomicInteger alone crosses that boundary.
Self-Check and Activities
Before looking at the sources, let us try to answer. Two requests both read 1: which results can their respective compareAndSet(1, 0) calls obtain, and why can they not both succeed? If a method uses getAndUpdate with a lambda sending a notification, which event can be duplicated? After all workers finish, what changes in reading LongAdder.sum() compared with a reading while they are still writing? Reasoned answers and practical tests are in the C20A activities; verification does not require guessing the order in which the scheduler will execute the threads.
The chapter's lesson is a question to carry into the rest of the book: what is the smallest decision that must remain indivisible, and which observers must see it that way? When the answer concerns a single variable in memory, an atomic class can be a clear tool. When the answer includes waiting, several mutable states or persistence, we broaden the model rather than asking a counter to do work that does not belong to it.
Essential References
Essential reference – Java SE 25 Specifications. Oracle's documentation for the
java.util.concurrent.atomicpackage defines the toolkit and its scope. The pages forAtomicInteger,AtomicReference,LongAdderandAtomicStampedReferencespecify the contracts used in the chapter. TheVarHandlepage documents the access modes mentioned in further reading. These are API specifications, rather than measurements of our application's performance: throughput advantages depend on the workload and must be verified separately.