Files
java-core-examples/cow

cow

Companion code for the ankurm.com post "CopyOnWriteArrayList in Java: When It Wins (JMH) and When It Hurts." Module in java-core-examples, the Java-core series.

Versions this was built and tested against

Component Version Notes
JDK 25.0.4.1+1 (Temurin, LTS) CopyOnWriteArrayList shipped in Java 5 (2004) as part of the original java.util.concurrent; unchanged in contract since, though it now also implements SequencedCollection (JEP 431, Java 21).
JMH 1.37
JUnit Jupiter 5.11.0
Maven 3.9.11

Quickstart

export JAVA_HOME=/path/to/jdk-17-or-newer
mvn compile
java -cp target/classes com.ankurm.cow.IteratorSnapshotDemo

scripts/run-all.sh regenerates output/01-05 (not 06, see below). scripts/run.sh <ClassName> runs one demo ad hoc. The JMH benchmarks also run as an executable jar: mvn package -DskipTests then java -jar target/benchmarks.jar <BenchmarkName>.

What's in here

File What it shows
IteratorSnapshotDemo.java Forces the exact ordering with CountDownLatch barriers instead of Thread.sleep guesses: an iterator opened before a write never sees that write; one opened after does. Deterministic every run.
SynchronizedListCmeDemo.java Reproduces ConcurrentModificationException on Collections.synchronizedList on purpose, deterministically, when a caller iterates without wrapping the loop in its own synchronized block - then shows the fix.
ReadThroughputBenchmark.java JMH: 4 threads reading by index, CopyOnWriteArrayList vs synchronizedList, with and without a concurrent background writer.
WriteThroughputBenchmark.java JMH: cost of an addLast/removeFirst pair at sizes 10 and 1000, with 1 writer and with 4 concurrent writers.
CowClaimsTest.java Pins all of the above as assertions, 7/7 passing.
output/01-05 Captured runs of the two demos, the test suite, and both JMH benchmarks.
output/06-write-throughput-race.txt See below - not something run-all.sh regenerates.

Notes worth knowing before reading the post

  • The internal lock is a plain Object monitor, not a ReentrantLock. javap -p java.util.concurrent.CopyOnWriteArrayList on JDK 25 shows final transient java.lang.Object lock; - a lot of writing (including earlier drafts of this very post) describes it as "a global reentrant lock." That description matches older JDKs; verify against the jar you're actually running before repeating it.
  • output/06-write-throughput-race.txt is a deliberately preserved bug, not a regenerable artifact. The first version of WriteThroughputBenchmark used list.add(-1); list.remove(list.size() - 1); - two separate calls, each individually thread-safe, but not atomic as a pair. Under 4 concurrent writer threads this raced and threw a real ArrayIndexOutOfBoundsException on CopyOnWriteArrayList and a real IndexOutOfBoundsException on Collections.synchronizedList - both captured verbatim in that file. The fix, addLast()/removeFirst() (JEP 431 default-turned-overridden methods, each a single call with no externally-fetched index), is what the committed benchmark actually runs. Left in deliberately: it's a more convincing demonstration of "thread-safe per-call does not mean safe as a sequence of calls" than a paragraph describing the same thing would be.
  • Read throughput numbers are dramatic, not subtle. See the post for the real ratio - this is the headline number and it's larger than most people expect even having read "CopyOnWriteArrayList is for read-heavy workloads" a dozen times.
  • This sandbox is shared and multi-tenant. Several rows in output/05 carry an error margin wider than the mean. Read the shape (which variant is bigger, by roughly how much) rather than quoting any single ops/ms figure to three significant digits.

License

MIT - see the repo-wide LICENSE.