The rule that must survive interleaving
Start with the invariant. For available units, each successful reservation must remove one unit and the count must never be negative. Two calls may observe the same value before either changes it. If both read one and then decrement, two reservations consume a single unit. This remains possible even when each read and decrement is individually atomic.
volatile provides certain visibility and ordering relationships, but does not turn count++ into one update. The expression reads, computes and writes. Two threads may read zero and both write one. Making a reference final also does not make the referenced object immutable or a compound operation atomic.
Choose a boundary protecting the entire decision: a shared lock over condition and update, or an atomic conditional update that revalidates state. Avoid diagnosing only by observed frequency. One run without data loss does not prove that all allowed interleavings preserve the invariant.
A monitor has an identity
An instance synchronized method uses that instance’s monitor. If state is static and two instances modify it using their own monitors, calls lack shared exclusion. The modifier is present but protection does not match the resource. Every involved access path must follow the same policy.
With multiple locks, acquisition order is part of the design. If one participant holds A while waiting for B and another holds B while waiting for A, neither can advance. A global order followed by all relevant paths removes that particular cycle. It does not establish absence of all stalls involving other dependencies.
Adding sleep neither releases the monitor nor creates synchronization that repairs unprotected access. It may merely alter the likelihood of observing the problem. In incident analysis, identify the locked object, its owner, its waiters and the work needed to release it. Identical variable names do not establish that two paths use the same object.
Wait for a condition
In a monitor protocol, wait releases the corresponding monitor while waiting and reacquires it before returning normally. This allows a producer to enter, change state and notify. Check the condition under the same lock used to change it.
Use a loop around wait. A thread may wake without the expected notification, or another consumer may change the condition before it reacquires the monitor. Notification means reevaluation is worthwhile; it does not automatically grant the resource. notifyAll wakes participants, but they still compete for the monitor and evaluate their conditions.
Interruption is another exit path. If waiting throws InterruptedException, the routine needs a policy: propagate to a deciding layer, or exit while preserving the signal when unable to declare that exception. Ignoring interruption and immediately waiting again can prevent requested shutdown. Tie the decision to the routine’s responsibility without treating every interruption as success.
Future describes a result, not every effect
submit returns a Future for tracking the task. get may wait for a normal result or report task failure through ExecutionException. Cancellation is another control outcome. isDone does not necessarily mean success: it also covers failure and cancellation.
A timeout supplied to get limits caller waiting. TimeoutException does not establish that the task stopped, never started or performed no external write. If the caller intends to repeat the operation, it needs the previous work’s state and an identity or reconciliation policy for effects.
The same distinction applies to cancel(true). The request may cause interruption, but the underlying operation may not cooperate immediately. Even after observing isCancelled, confirm the outcome of a write already started. No automatic rollback covers databases, files or remote requests. Record Future state, task termination and functional outcome separately.
Shut down and retain capacity to progress
shutdown closes admission to new tasks and permits processing of submitted work. It does not wait for completion. awaitTermination observes termination with a timeout; the caller must interpret the result and respond to its own interruption. shutdownNow makes a stronger attempt to interrupt work but does not guarantee every task terminates.
Execution capacity is also a dependency. In a one-worker executor, an outer task submitting another task and waiting for it occupies the only slot the inner task needs. The queue contains work, but that work cannot start while the outer task keeps waiting. Increasing only the timeout creates no additional worker.
In this lesson’s case, the inner calculation is finite and may execute directly inside the outer task, removing the dependency. Other designs may need result composition without blocking that executor or separate execution responsibilities. Match the change to real dependencies; indiscriminately adding threads may merely move the limit to connections, memory or external services.
Atomic updates and repeated effects
compareAndSet compares the current value with the expected value and replaces it only when they match. A failed attempt requires rereading and deciding again. In the reservation counter, successful replacement grants the unit; an earlier positive read does not.
Functions used by updateAndGet or getAndUpdate may be evaluated again under contention. They should therefore compute without external effects. Placing audit.record inside the function may produce multiple records for one eventually accepted update. Synchronizing only the audit write does not prevent another function evaluation.
Moving auditing after the update avoids that function-retry pattern but creates no transaction between the counter and audit destination. If recording fails after reservation, define how that state is recognized, recovered or compensated. Atomicity of one variable is a bounded guarantee. Do not present it as exactly-once behavior for a process involving different systems.
Guided review: the last unit
The snippet keeps the counter private and permits reservations only, without replenishment or resets. Its constructor rejects negative values. In tryReserve, reading zero returns false; a positive value permits attempting replacement with one fewer unit. If another caller won meanwhile, the loop returns to reading.
With one unit and two callers, both may read one. Only one compareAndSet from one to zero succeeds; the other rereads zero and fails to reserve. The count stays nonnegative and only an accepted replacement returns true. The method neither publishes a reservation externally nor promises priority among contenders.
Review whether other paths modify the counter or perform effects before successful replacement. Such changes can invalidate the assumptions. In this bounded scenario, tracing reads and attempts answers the question without running threads. Exercises require single or multiple selections, not reproducing a race on the learner’s machine.
import java.util.concurrent.atomic.AtomicInteger;
final class Reservations {
private final AtomicInteger available;
Reservations(int initial) {
if (initial < 0) throw new IllegalArgumentException("Negative stock");
available = new AtomicInteger(initial);
}
boolean tryReserve() {
for (;;) {
int current = available.get();
if (current == 0) return false;
if (available.compareAndSet(current, current - 1)) return true;
}
}
}
With one unit, two reads may see one, but only one replacement of one with zero grants a reservation. The losing attempt rechecks the counter.
Common pitfalls
Confusing volatile with exclusion; using different locks for shared state; waiting for work on the sole occupied worker; treating timeout as cancellation; writing effects inside a retried function.
Related topics: Methods, contracts and control flow · Exceptions, resources and files · Streams, lambdas and absence · Decimals, time and boundaries · Modules, delivery and diagnosis
Protect the complete decision and identify what each signal confirms: visibility, update, end of waiting, termination or functional outcome.
Reference: JLS 25: Threads and locks · Java SE 25; no preview features