liveBook · Manning

Concurrent modification

As explained in 100 Java Mistakes and How to Avoid Them

Concurrent modification is a structural change to a collection or map while an operation on that same structure is in progress.

Concurrent modification is a structural change to a collection or map while an operation on that same structure is still in progress. It can occur in one thread: adding to a list inside a for-each loop, or changing a cache from a mapping function that is already computing an entry. section 8.0 section 8.12 section 8.8

Non-concurrent Java collections generally forbid structural changes during iteration, except changes made through the iterator performing that iteration. Enhanced for-each syntax uses an iterator. Standard collections use fail-fast detection: a collection increments modCount on modification, an iterator keeps its expected value, and next() throws ConcurrentModificationException when the values differ. section 8.8

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
  System.out.println(s);
  if (s.equals("b")) {
    list.add("x");
  }
}

Adding "x" directly to the list after visiting "b" changes the list behind the for-each loop’s iterator. The next iterator advance detects the changed modification count and throws ConcurrentModificationException. section 8.8

Fail-fast behavior is detection, not a safe way to coordinate changes. An indexed loop has no iterator and may complete after seeing an added element; an iterator can also miss a modification when it has already reached the end. Avoid relying on either outcome. If iteration must proceed while modifying the original collection, iterate over a copy instead. section 8.8 section 8.8

A recursive cache inside computeIfAbsent

A cache of transitive expression references may call transitiveRefs for dependencies while computeIfAbsent is still computing the original key. That recursive lookup attempts to add another cache entry during the first map insertion. In Java 8 this could corrupt a HashMap; since Java 9, HashMap can detect the modification and computeIfAbsent may throw ConcurrentModificationException. Split lookup, computation, and put into separate steps when recursive cache population can occur. section 8.12 section 8.12

final Map<String, Set<String>> refCache = new HashMap<>();

Set<String> transitiveRefs(String variable) {
  return refCache.computeIfAbsent(variable, this::calc);
}

private Set<String> calc(String variable) {
  Set<String> result = new HashSet<>();
  String expression = map.getOrDefault(variable, "");
  for (String ref : immediateRefs(expression)) {
    result.add(ref);
    result.addAll(transitiveRefs(ref));
  }
  return result;
}

The mapping function calc recursively calls transitiveRefs, which re-enters refCache.computeIfAbsent before the outer computation has inserted its result. section 8.12

How 100 Java Mistakes and How to Avoid Them covers it

How deep it goes
Explains fail-fast iteration through modCount and traces recursive cache mutation through computeIfAbsent.
Examples
Uses list additions and removals during loops, copied iteration, and a recursive expression-reference cache.
What it assumes
Assumes familiarity with Java lists, maps, and loops while explaining enhanced for-loop desugaring into an iterator.
What you can do after
Diagnose/debug with it by locating structural changes hidden inside iteration or mapping functions and separating unsafe cache updates.
How it connects
Connects Iterator, enhanced for-loop, ConcurrentModificationException, HashMap, computeIfAbsent, and caching.

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.