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
Objectmonitor, not aReentrantLock.javap -p java.util.concurrent.CopyOnWriteArrayListon JDK 25 showsfinal 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.txtis a deliberately preserved bug, not a regenerable artifact. The first version ofWriteThroughputBenchmarkusedlist.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 realArrayIndexOutOfBoundsExceptiononCopyOnWriteArrayListand a realIndexOutOfBoundsExceptiononCollections.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/05carry 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.