diff --git a/cow/README.md b/cow/README.md index 5ca2f32..b48c335 100644 --- a/cow/README.md +++ b/cow/README.md @@ -20,9 +20,10 @@ 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 ` +`scripts/run-all.sh` regenerates `output/01-06` (not `07`, see below). `scripts/run.sh ` runs one demo ad hoc. The JMH benchmarks also run as an executable jar: `mvn package -DskipTests` -then `java -jar target/benchmarks.jar `. +then `java -jar target/benchmarks.jar `. 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 @@ -34,12 +35,13 @@ then `java -jar target/benchmarks.jar `. | `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. | +| `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 - **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. - **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. diff --git a/cow/output/06-javap-lock-field.txt b/cow/output/06-javap-lock-field.txt new file mode 100644 index 0000000..a71fb57 --- /dev/null +++ b/cow/output/06-javap-lock-field.txt @@ -0,0 +1,3 @@ +$ javap -p java.util.concurrent.CopyOnWriteArrayList | grep -i lock + final transient java.lang.Object lock; + private void resetLock(); diff --git a/cow/output/06-write-throughput-race.txt b/cow/output/07-write-throughput-race.txt similarity index 100% rename from cow/output/06-write-throughput-race.txt rename to cow/output/07-write-throughput-race.txt diff --git a/cow/scripts/run-all.sh b/cow/scripts/run-all.sh index 1f804f0..edcab93 100644 --- a/cow/scripts/run-all.sh +++ b/cow/scripts/run-all.sh @@ -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 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 "is NOT regenerated by this script."