cow: add committed javap evidence for the internal lock field, renumber the preserved race-condition output to make room for it

This commit is contained in:
Claude
2026-10-01 12:19:08 +00:00
parent 434054f246
commit 6520e7aabd
4 changed files with 14 additions and 5 deletions
+6 -4
View File
@@ -20,9 +20,10 @@ mvn compile
java -cp target/classes com.ankurm.cow.IteratorSnapshotDemo java -cp target/classes com.ankurm.cow.IteratorSnapshotDemo
``` ```
`scripts/run-all.sh` regenerates `output/01-05` (not `06`, see below). `scripts/run.sh <ClassName>` `scripts/run-all.sh` regenerates `output/01-06` (not `07`, see below). `scripts/run.sh <ClassName>`
runs one demo ad hoc. The JMH benchmarks also run as an executable jar: `mvn package -DskipTests` runs one demo ad hoc. The JMH benchmarks also run as an executable jar: `mvn package -DskipTests`
then `java -jar target/benchmarks.jar <BenchmarkName>`. then `java -jar target/benchmarks.jar <BenchmarkName>`. The lock-field check is a one-line `javap`
command, also reproducible directly: `javap -p java.util.concurrent.CopyOnWriteArrayList | grep -i lock`.
## What's in here ## What's in here
@@ -34,12 +35,13 @@ then `java -jar target/benchmarks.jar <BenchmarkName>`.
| `WriteThroughputBenchmark.java` | JMH: cost of an `addLast`/`removeFirst` pair at sizes 10 and 1000, with 1 writer and with 4 concurrent writers. | | `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. | | `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/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. | | `output/06-javap-lock-field.txt` | `javap -p` output for the internal lock field - see below. |
| `output/07-write-throughput-race.txt` | See below - not something `run-all.sh` regenerates. |
## Notes worth knowing before reading the post ## 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. - **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. - **`output/07-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. - **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. - **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.
+3
View File
@@ -0,0 +1,3 @@
$ javap -p java.util.concurrent.CopyOnWriteArrayList | grep -i lock
final transient java.lang.Object lock;
private void resetLock();
+5 -1
View File
@@ -16,6 +16,10 @@ mvn -q -B package -DskipTests
java -jar target/benchmarks.jar ReadThroughputBenchmark -rf text -rff output/04-read-throughput-jmh.txt java -jar target/benchmarks.jar ReadThroughputBenchmark -rf text -rff output/04-read-throughput-jmh.txt
java -jar target/benchmarks.jar WriteThroughputBenchmark -rf text -rff output/05-write-throughput-jmh.txt java -jar target/benchmarks.jar WriteThroughputBenchmark -rf text -rff output/05-write-throughput-jmh.txt
echo "Regenerated output/01-05. output/06-write-throughput-race.txt is a one-off captured failure" { echo '$ javap -p java.util.concurrent.CopyOnWriteArrayList | grep -i lock'; \
javap -p java.util.concurrent.CopyOnWriteArrayList 2>&1 | grep -vE 'JAVA_TOOL_OPTIONS|^WARNING' | grep -i lock; } \
> output/06-javap-lock-field.txt
echo "Regenerated output/01-06. output/07-write-throughput-race.txt is a one-off captured failure"
echo "from an earlier, deliberately buggy version of the write benchmark - see the README - and" echo "from an earlier, deliberately buggy version of the write benchmark - see the README - and"
echo "is NOT regenerated by this script." echo "is NOT regenerated by this script."