liveBook · Manning

Atomicity

As explained in 100 Java Mistakes and How to Avoid Them

An atomic request is an operation that observes or updates its target as one indivisible action without an intervening state change.

Atomicity means performing an observation or update as one indivisible action: no relevant state change can slip between the parts of the operation. The common mistake is splitting a decision and its action into separate calls, then assuming the earlier result is still true. That assumption fails for filesystem metadata, time, weak references, and data shared with other threads. section 9.11 section 9.13

A pair of individually valid calls is not necessarily valid together. Checking whether a path is a regular file and then asking for its size can observe different filesystem states. Likewise, checking a file's size before reading it does not guarantee that the subsequent read remains within the limit: the file can grow between calls. When the required result is “read no more than this limit,” enforce that limit while reading and then detect whether more content remains. section 9.11 section 9.11

static byte[] readFileIfItsNotTooLarge(Path path) throws IOException {
  final int maxSize = 1024 * 1024;
  try (InputStream is = Files.newInputStream(path)) {
    byte[] bytes = is.readNBytes(maxSize);
    if (bytes.length == maxSize && is.read() != -1) {
      throw new UnsupportedOperationException("File is too large");
    }
    return bytes;
  }
}

Reading up to a maximum and checking for an additional byte avoids relying on a size query that may become outdated before the file is read. section 9.11

Concurrent collections make split operations especially hazardous. `isEmpty()` followed by `remove()` performs two queue interactions; another consumer can empty the queue after the check, causing `remove()` to fail. Prefer a single operation whose result carries both outcomes. `poll()` removes an element when available and otherwise returns `null`, so the null check concerns a local value rather than a queue state another thread can change. section 9.12 section 9.12

void takeAndProcess(Queue<String> queue) {
  String element = queue.poll();
  if (element != null) {
    … // Process element
  }
}

`poll()` combines availability testing and removal, then lets processing depend on the returned local value. section 9.12

Use operations designed to make the whole update atomic rather than assembling one from reads and writes:

  • For concurrent maps, replace get-and-put counting sequences with `merge()`; Java 8 also provides `putIfAbsent()`, `compute()`, `computeIfAbsent()`, and `computeIfPresent()` for common atomic update cases. section 9.12
  • For an `AtomicInteger`, do not implement doubling as `get()`, multiplication, and `set()`. Use `updateAndGet(value -> value * 2)` so concurrent updates do not lose a multiplication. section 9.12
  • For values that should remain stable within one computation, such as current time, query once and reuse the stored result. section 9.11
  • Treat stability assumptions as contextual: an application-owned data folder may be safely assumed untouched in some cases, but user-specified folders require caution. section 9.11

Counting without a lost update

A concurrent map counter written as `get`, increment, then `put` can overwrite another thread's increment because both threads may derive a value from the same old entry. `map.merge(key, 1, Integer::sum)` delegates the increment to one atomic map operation, so the entry is incremented atomically when the map implementation is correct. section 9.12

Atomic operations are preferred whenever a changing target is queried or updated across multiple steps. They do not require treating every environment as hostile, but they prevent correctness from depending on an unstated promise that nothing changes between two requests. section 9.11 section 9.13

How 100 Java Mistakes and How to Avoid Them covers it

How deep it goes
Explains concrete races and replaces split calls with bounded reads, Queue.poll, ConcurrentMap methods, and AtomicInteger.updateAndGet.
Examples
Contrasts file-size checks with bounded stream reads, isEmpty-plus-remove with poll, map counting with merge, and get-set with updateAndGet.
What it assumes
Assumes Java familiarity with filesystem APIs, collections, and concurrent access; it does not teach concurrency fundamentals from zero.
What you can do after
Apply it via Java filesystem, Queue, ConcurrentMap, and AtomicInteger APIs to avoid races caused by split observations and updates.
How it connects
Connects atomicity to filesystem metadata, concurrent collections, ConcurrentMap, Queue, AtomicInteger, and current time.

Also explained in

Compare how 13 books explain atomicity →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Java Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.