Skip to content

Commit d2940c3

Browse files
jderegclaude
andcommitted
Make lock contention tracking opt-in for better write throughput
Contention metrics (AtomicInteger increments, tryLock probing) added measurable overhead on every put/remove. Now disabled by default; enable via builder().trackContentionMetrics(true) when diagnostics are needed. When off, each write does a single lock() instead of tryLock()+lock() plus 2+ atomic counter increments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 484bddf commit d2940c3

3 files changed

Lines changed: 92 additions & 44 deletions

File tree

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
* **PERFORMANCE**: `MultiKeyMap` - `keySet()` and `values()` no longer rebuild full `entrySet()`
5050
* `values()` now iterates buckets directly, skipping all key reconstruction (`reconstructKey()`) and `SimpleEntry` allocation
5151
* `keySet()` now iterates buckets directly, skipping `SimpleEntry` wrapper allocation
52+
* **PERFORMANCE**: `MultiKeyMap` - Lock contention tracking is now opt-in via `trackContentionMetrics(true)`
53+
* Eliminates 2+ `AtomicInteger` CAS operations per `put`/`remove` call when tracking is disabled (default)
54+
* Also skips the `tryLock()`-then-`lock()` contention detection pattern, using a single `lock()` call instead
55+
* Enable via `MultiKeyMap.builder().trackContentionMetrics(true)` when diagnostics are needed
5256

5357
#### 4.90.0 2026-02-02
5458
* **BUG FIX**: `DeepEquals` - URL comparison now uses string representation instead of `URL.equals()`

src/main/java/com/cedarsoftware/util/MultiKeyMap.java

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ public enum CollectionKeyMode {
422422
private final boolean simpleKeysMode;
423423
private final boolean valueBasedEquality;
424424
private final boolean caseSensitive;
425+
private final boolean trackContentionMetrics;
425426
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
426427

427428
// Cached hashCode for performance (invalidated on mutations)
@@ -518,6 +519,7 @@ private MultiKeyMap(Builder<V> builder) {
518519
this.simpleKeysMode = builder.simpleKeysMode;
519520
this.valueBasedEquality = builder.valueBasedEquality;
520521
this.caseSensitive = builder.caseSensitive;
522+
this.trackContentionMetrics = builder.trackContentionMetrics;
521523

522524
for (int i = 0; i < STRIPE_COUNT; i++) {
523525
stripeLocks[i] = new ReentrantLock();
@@ -590,6 +592,7 @@ public static class Builder<V> {
590592
private boolean simpleKeysMode = false;
591593
private boolean valueBasedEquality = true; // Default: cross-type numeric matching
592594
private boolean caseSensitive = true; // Default: case-sensitive string comparison
595+
private boolean trackContentionMetrics = false; // Default: off for maximum write throughput
593596

594597
// Private constructor - instantiate via MultiKeyMap.builder()
595598
private Builder() {}
@@ -710,6 +713,22 @@ public Builder<V> caseSensitive(boolean caseSensitive) {
710713
return this;
711714
}
712715

716+
/**
717+
* Enables or disables lock contention metrics tracking.
718+
* <p>When enabled, the map tracks per-stripe lock acquisition counts and contention
719+
* events using atomic counters. This adds measurable overhead (multiple {@code AtomicInteger}
720+
* increments per write operation) but provides diagnostic data accessible via
721+
* {@link MultiKeyMap#printContentionStatistics()}.</p>
722+
* <p>Default is {@code false} (disabled) for maximum write throughput.</p>
723+
*
724+
* @param track {@code true} to enable contention tracking, {@code false} to disable
725+
* @return this builder instance for method chaining
726+
*/
727+
public Builder<V> trackContentionMetrics(boolean track) {
728+
this.trackContentionMetrics = track;
729+
return this;
730+
}
731+
713732
/**
714733
* Copies configuration from an existing MultiKeyMap.
715734
* <p>This copies all configuration settings including capacity, load factor,
@@ -726,6 +745,7 @@ public Builder<V> from(MultiKeyMap<?> source) {
726745
this.simpleKeysMode = source.simpleKeysMode;
727746
this.valueBasedEquality = source.valueBasedEquality;
728747
this.caseSensitive = source.caseSensitive;
748+
this.trackContentionMetrics = source.trackContentionMetrics;
729749
return this;
730750
}
731751

@@ -992,16 +1012,21 @@ private ReentrantLock getStripeLock(int hash) {
9921012
}
9931013

9941014
private void lockAllStripes() {
995-
int contended = 0;
996-
for (ReentrantLock lock : stripeLocks) {
997-
// Use tryLock() to accurately detect contention
998-
if (!lock.tryLock()) {
999-
contended++;
1000-
lock.lock(); // Now wait for the lock
1015+
if (trackContentionMetrics) {
1016+
int contended = 0;
1017+
for (ReentrantLock lock : stripeLocks) {
1018+
if (!lock.tryLock()) {
1019+
contended++;
1020+
lock.lock();
1021+
}
1022+
}
1023+
globalLockAcquisitions.incrementAndGet();
1024+
if (contended > 0) globalLockContentions.incrementAndGet();
1025+
} else {
1026+
for (ReentrantLock lock : stripeLocks) {
1027+
lock.lock();
10011028
}
10021029
}
1003-
globalLockAcquisitions.incrementAndGet();
1004-
if (contended > 0) globalLockContentions.incrementAndGet();
10051030
}
10061031

10071032
private void unlockAllStripes() {
@@ -3114,23 +3139,29 @@ private V putInternal(MultiKey<V> newKey) {
31143139
V old;
31153140
boolean resize;
31163141

3117-
// Use tryLock() to accurately detect contention
3118-
boolean contended = !lock.tryLock();
3119-
if (contended) {
3120-
// Failed to acquire immediately - this is true contention
3121-
lock.lock(); // Now wait for the lock
3122-
contentionCount.incrementAndGet();
3123-
stripeLockContention[stripe].incrementAndGet();
3124-
}
3125-
3126-
try {
3127-
totalLockAcquisitions.incrementAndGet();
3128-
stripeLockAcquisitions[stripe].incrementAndGet();
3129-
3130-
old = putNoLock(newKey);
3131-
resize = atomicSize.get() > buckets.length() * loadFactor;
3132-
} finally {
3133-
lock.unlock();
3142+
if (trackContentionMetrics) {
3143+
boolean contended = !lock.tryLock();
3144+
if (contended) {
3145+
lock.lock();
3146+
contentionCount.incrementAndGet();
3147+
stripeLockContention[stripe].incrementAndGet();
3148+
}
3149+
try {
3150+
totalLockAcquisitions.incrementAndGet();
3151+
stripeLockAcquisitions[stripe].incrementAndGet();
3152+
old = putNoLock(newKey);
3153+
resize = atomicSize.get() > buckets.length() * loadFactor;
3154+
} finally {
3155+
lock.unlock();
3156+
}
3157+
} else {
3158+
lock.lock();
3159+
try {
3160+
old = putNoLock(newKey);
3161+
resize = atomicSize.get() > buckets.length() * loadFactor;
3162+
} finally {
3163+
lock.unlock();
3164+
}
31343165
}
31353166

31363167
resizeRequest(resize);
@@ -3355,22 +3386,27 @@ private V removeInternal(final MultiKey<V> removeKey) {
33553386
ReentrantLock lock = stripeLocks[stripe];
33563387
V old;
33573388

3358-
// Use tryLock() to accurately detect contention
3359-
boolean contended = !lock.tryLock();
3360-
if (contended) {
3361-
// Failed to acquire immediately - this is true contention
3362-
lock.lock(); // Now wait for the lock
3363-
contentionCount.incrementAndGet();
3364-
stripeLockContention[stripe].incrementAndGet();
3365-
}
3366-
3367-
try {
3368-
totalLockAcquisitions.incrementAndGet();
3369-
stripeLockAcquisitions[stripe].incrementAndGet();
3370-
3371-
old = removeNoLock(removeKey);
3372-
} finally {
3373-
lock.unlock();
3389+
if (trackContentionMetrics) {
3390+
boolean contended = !lock.tryLock();
3391+
if (contended) {
3392+
lock.lock();
3393+
contentionCount.incrementAndGet();
3394+
stripeLockContention[stripe].incrementAndGet();
3395+
}
3396+
try {
3397+
totalLockAcquisitions.incrementAndGet();
3398+
stripeLockAcquisitions[stripe].incrementAndGet();
3399+
old = removeNoLock(removeKey);
3400+
} finally {
3401+
lock.unlock();
3402+
}
3403+
} else {
3404+
lock.lock();
3405+
try {
3406+
old = removeNoLock(removeKey);
3407+
} finally {
3408+
lock.unlock();
3409+
}
33743410
}
33753411

33763412
return old;

src/test/java/com/cedarsoftware/util/MultiKeyMapStripeTrackingTest.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,17 @@ private static int spread(int h) {
2626
return h ^ (h >>> 16);
2727
}
2828

29+
/** Creates a small map with contention tracking enabled */
30+
private static MultiKeyMap<String> createTrackedMap() {
31+
return MultiKeyMap.<String>builder()
32+
.capacity(16)
33+
.trackContentionMetrics(true)
34+
.build();
35+
}
36+
2937
@Test
3038
void testPutTracksAcquisitionOnCorrectStripe() throws Exception {
31-
MultiKeyMap<String> map = new MultiKeyMap<>(16);
39+
MultiKeyMap<String> map = createTrackedMap();
3240

3341
// Access internals via reflection
3442
Field stripeAcqField = MultiKeyMap.class.getDeclaredField("stripeLockAcquisitions");
@@ -78,7 +86,7 @@ void testPutTracksAcquisitionOnCorrectStripe() throws Exception {
7886

7987
@Test
8088
void testRemoveTracksAcquisitionOnCorrectStripe() throws Exception {
81-
MultiKeyMap<String> map = new MultiKeyMap<>(16);
89+
MultiKeyMap<String> map = createTrackedMap();
8290

8391
Field stripeAcqField = MultiKeyMap.class.getDeclaredField("stripeLockAcquisitions");
8492
stripeAcqField.setAccessible(true);
@@ -127,7 +135,7 @@ void testRemoveTracksAcquisitionOnCorrectStripe() throws Exception {
127135

128136
@Test
129137
void testNoAcquisitionsAboveTableSizeStripes() throws Exception {
130-
MultiKeyMap<String> map = new MultiKeyMap<>(16);
138+
MultiKeyMap<String> map = createTrackedMap();
131139

132140
Field stripeAcqField = MultiKeyMap.class.getDeclaredField("stripeLockAcquisitions");
133141
stripeAcqField.setAccessible(true);

0 commit comments

Comments
 (0)