Add lru-cache module: LinkedHashMap and hand-rolled DLL LRU, coarse-lock/striped thread-safe variants, broken ReadWriteLock demo, Caffeine reference, 43 tests, JMH harness

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
2026-10-29 05:52:27 +00:00
co-authored by Claude Sonnet 5.5
parent 4a376b94e9
commit 0457cf4bfb
31 changed files with 1425 additions and 0 deletions
@@ -0,0 +1,17 @@
Dry run, capacity 3: state after each operation (keys listed LRU -> MRU)
========================================================================
# operation returns LRU -> MRU ptr-writes same as LinkedHashMap?
1 put(1,A) - [1] 4 yes
2 put(2,B) - [1,2] 4 yes
3 put(3,C) - [1,2,3] 4 yes
4 get(1) A [2,3,1] 6 yes
5 put(4,D) - (evicts 2) [3,1,4] 6 yes
6 get(2) null [3,1,4] 0 yes
7 put(3,C2) - (replace, refresh) [1,4,3] 6 yes
8 put(5,E) - (evicts 1) [4,3,5] 6 yes
9 get(4) D [3,5,4] 6 yes
10 get(1) null [3,5,4] 0 yes
ptr-writes = pointer assignments on list nodes made by HashMapLruCache for that operation.
A miss costs 0, a hit costs 6 (unlink 2 + link 4), a new insert 4 (+2 for the eviction unlink). None depends on the cache size.
@@ -0,0 +1,9 @@
Pointer writes per HashMapLruCache operation, by cache size
===========================================================
capacity get(hit) get(miss) put(new) put(new, evicts LRU)
10 6 0 4 6
1000 6 0 4 6
100000 6 0 4 6
get(hit) is measured on the OLDEST key (position n from the front). Cost is 6 writes anyway: the node is reached through the hash index, not by walking the list.
@@ -0,0 +1,12 @@
LinkedHashMap(accessOrder=true), start [1, 2, 3] (LRU -> MRU): does the call move key 1?
========================================================================================
get(1) -> order [2, 3, 1] key 1 moved to MRU end: true
put(1, "x") (existing key) -> order [2, 3, 1] key 1 moved to MRU end: true
getOrDefault(1, "-") -> order [2, 3, 1] key 1 moved to MRU end: true
putIfAbsent(1, "x") (key present) -> order [2, 3, 1] key 1 moved to MRU end: true
computeIfAbsent(1, k -> "x") (key present) -> order [2, 3, 1] key 1 moved to MRU end: true
containsKey(1) -> order [1, 2, 3] key 1 moved to MRU end: false
keySet() iteration -> order [1, 2, 3] key 1 moved to MRU end: false
entrySet() iteration -> order [1, 2, 3] key 1 moved to MRU end: false
size() -> order [1, 2, 3] key 1 moved to MRU end: false
@@ -0,0 +1,7 @@
In access-order mode get() is a structural modification (single thread, no concurrency)
=======================================================================================
for (Integer k : accessOrderMap.keySet()) accessOrderMap.get(k);
-> java.util.ConcurrentModificationException
same loop on an insertion-order LinkedHashMap -> completes normally
@@ -0,0 +1,9 @@
8 threads x 60000 ops (80% get / 20% put), capacity 64, 256 keys
================================================================
implementation exceptions wrong values size>capacity seen? invariants post-stress LRU check
SynchronizedLruCache 0 0 no ok ok
ReentrantLockLruCache 0 0 no ok ok
StripedLruCache(8) 0 0 no ok n/a (per-segment LRU)
Hit/miss counts are deliberately not printed: they differ on every run and nothing asserts them.
@@ -0,0 +1,7 @@
StripedLruCache(capacity 4, 2 stripes): even keys -> stripe 0, odd keys -> stripe 1
===================================================================================
put(1) put(0) put(2) put(4) // key 1 is the globally least recently used
get(0) -> null (evicted: stripe 0 held {0,2} and was full)
get(1) -> present (a strict global LRU would have evicted it instead)
size() = 3 of capacity 4 (evicted while not full)
+15
View File
@@ -0,0 +1,15 @@
Caffeine 3.3.1 maximumSize(3) given the dry-run operations (same-thread executor, cleanUp after each)
====================================================================================================
put(1,A) -> resident keys (unordered): [1]
put(2,B) -> resident keys (unordered): [1, 2]
put(3,C) -> resident keys (unordered): [1, 2, 3]
get(1) -> resident keys (unordered): [1, 2, 3]
put(4,D) -> resident keys (unordered): [1, 2, 4]
get(2) -> resident keys (unordered): [1, 2, 4]
put(3,C2) -> resident keys (unordered): [1, 2, 3]
put(5,E) -> resident keys (unordered): [1, 2, 5]
get(4) -> resident keys (unordered): [1, 2, 5]
get(1) -> resident keys (unordered): [1, 2, 5]
A strict LRU of capacity 3 ends this sequence with [3, 5, 4] (see 01-dry-run-capacity-3.txt).
@@ -0,0 +1,9 @@
Keys 0..2047 that land in segment 0 of 16: how many distinct buckets of that segment's 128-slot HashMap do they use?
====================================================================================================
(bucket = (h ^ (h >>> 16)) & 127, the formula java.util.HashMap uses; computed arithmetically, not by reflection)
LOW_BITS keys in segment 0: 128 buckets used: 8 of 128 longest bucket: 16 keys
MIXED keys in segment 0: 129 buckets used: 65 of 128 longest bucket: 4 keys
Each of those 8 buckets holds 16 keys: past the treeify threshold of 8 (the table has >= 64 slots), so java.util.HashMap turns them into trees.
@@ -0,0 +1,18 @@
cores=2 threads=4 opsPerThread=400000 capacity=64 keys=128
workload: 90% get, 10% put, uniform random keys
round 1
ReadWriteLock, get under READ lock worker exceptions=0; size=64/64; INVARIANT BROKEN: list length 17 != index size 64
synchronized (correct) worker exceptions=0; size=64/64; invariants OK
round 2
ReadWriteLock, get under READ lock worker exceptions=0; size=64/64; INVARIANT BROKEN: list node not in index: 94
synchronized (correct) worker exceptions=0; size=64/64; invariants OK
round 3
ReadWriteLock, get under READ lock worker exceptions=0; size=64/64; invariants OK
synchronized (correct) worker exceptions=0; size=64/64; invariants OK
round 4
ReadWriteLock, get under READ lock worker exceptions=0; size=64/64; INVARIANT BROKEN: list node not in index: 125
synchronized (correct) worker exceptions=0; size=64/64; invariants OK
round 5
ReadWriteLock, get under READ lock worker exceptions=0; size=64/64; invariants OK
synchronized (correct) worker exceptions=0; size=64/64; invariants OK
+32
View File
@@ -0,0 +1,32 @@
JMH 1.37 / openjdk version "25.0.5" 2026-10-20 LTS
cores=2 loadavg-at-start=24.40 26.08 18.95 (shared sandbox: INDICATIVE)
workload: get; on miss put. Keys uniform over 2048, capacity 1024. ops/microsecond, higher is better.
### 1 thread, all implementations
Benchmark (impl) Mode Cnt Score Error Units
LruBenchmark.lookupThenFillOnMiss linkedhashmap-unsafe thrpt 5 4.908 ± 0.912 ops/us
LruBenchmark.lookupThenFillOnMiss dll-unsafe thrpt 5 3.946 ± 3.172 ops/us
LruBenchmark.lookupThenFillOnMiss synchronizedMap(LinkedHashMap) thrpt 5 2.619 ± 3.358 ops/us
LruBenchmark.lookupThenFillOnMiss synchronized thrpt 5 2.832 ± 1.957 ops/us
LruBenchmark.lookupThenFillOnMiss reentrantlock thrpt 5 3.089 ± 1.593 ops/us
LruBenchmark.lookupThenFillOnMiss striped16 thrpt 5 2.389 ± 1.758 ops/us
LruBenchmark.lookupThenFillOnMiss striped16-lowbits thrpt 5 1.337 ± 0.436 ops/us
LruBenchmark.lookupThenFillOnMiss caffeine thrpt 5 1.307 ± 2.076 ops/us
### 2 threads, thread-safe implementations only
Benchmark (impl) Mode Cnt Score Error Units
LruBenchmark.lookupThenFillOnMiss synchronizedMap(LinkedHashMap) thrpt 5 2.807 ± 1.557 ops/us
LruBenchmark.lookupThenFillOnMiss synchronized thrpt 5 2.609 ± 1.794 ops/us
LruBenchmark.lookupThenFillOnMiss reentrantlock thrpt 5 2.665 ± 2.561 ops/us
LruBenchmark.lookupThenFillOnMiss striped16 thrpt 5 1.769 ± 1.727 ops/us
LruBenchmark.lookupThenFillOnMiss striped16-lowbits thrpt 5 1.048 ± 0.400 ops/us
LruBenchmark.lookupThenFillOnMiss caffeine thrpt 5 2.248 ± 4.427 ops/us
### 4 threads, thread-safe implementations only
Benchmark (impl) Mode Cnt Score Error Units
LruBenchmark.lookupThenFillOnMiss synchronizedMap(LinkedHashMap) thrpt 5 5.475 ± 5.365 ops/us
LruBenchmark.lookupThenFillOnMiss synchronized thrpt 5 4.926 ± 3.010 ops/us
LruBenchmark.lookupThenFillOnMiss reentrantlock thrpt 5 4.230 ± 3.008 ops/us
LruBenchmark.lookupThenFillOnMiss striped16 thrpt 5 2.399 ± 1.785 ops/us
LruBenchmark.lookupThenFillOnMiss striped16-lowbits thrpt 5 1.256 ± 0.635 ops/us
LruBenchmark.lookupThenFillOnMiss caffeine thrpt 5 1.821 ± 1.796 ops/us