cow: CopyOnWriteArrayList - deterministic iterator-snapshot and synchronizedList CME demos, JMH read/write throughput, and the addLast/removeFirst fix for a real concurrent-writer race
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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](../LICENSE).
|
||||
Reference in New Issue
Block a user