diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 6c10be9e73b..842128cff4d 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -33,11 +33,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.function.Function; import org.springframework.lang.Nullable; /** - * A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or + * A {@link ConcurrentHashMap} variant that uses {@link ReferenceType#SOFT soft} or * {@linkplain ReferenceType#WEAK weak} references for both {@code keys} and {@code values}. * *

This class can be used as an alternative to @@ -365,6 +367,118 @@ public class ConcurrentReferenceHashMap extends AbstractMap implemen }); } + @Override + @Nullable + public V computeIfAbsent(@Nullable K key, Function mappingFunction) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + if (entry != null) { + return entry.getValue(); + } + V value = mappingFunction.apply(key); + // Add entry only if not null + if (value != null) { + Assert.state(entries != null, "No entries segment"); + entries.add(value); + } + return value; + } + }); + } + + @Override + @Nullable + public V computeIfPresent(@Nullable K key, BiFunction remappingFunction) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + if (entry != null) { + V oldValue = entry.getValue(); + V value = remappingFunction.apply(key, oldValue); + if (value != null) { + // Replace entry + entry.setValue(value); + return value; + } + else { + // Remove entry + if (ref != null) { + ref.release(); + } + } + } + return null; + } + }); + } + + @Override + @Nullable + public V compute(@Nullable K key, BiFunction remappingFunction) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + V oldValue = null; + if (entry != null) { + oldValue = entry.getValue(); + } + V value = remappingFunction.apply(key, oldValue); + if (value != null) { + if (entry != null) { + // Replace entry + entry.setValue(value); + } + else { + // Add entry + Assert.state(entries != null, "No entries segment"); + entries.add(value); + } + return value; + } + else { + // Remove entry + if (ref != null) { + ref.release(); + } + } + return null; + } + }); + } + + @Override + @Nullable + public V merge(@Nullable K key, @Nullable V value, BiFunction remappingFunction) { + return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { + @Override + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + if (entry != null) { + V oldValue = entry.getValue(); + V newValue = remappingFunction.apply(oldValue, value); + if (newValue != null) { + // Replace entry + entry.setValue(newValue); + return newValue; + } + else { + // Remove entry + if (ref != null) { + ref.release(); + } + return null; + } + } + else { + // Add entry + Assert.state(entries != null, "No entries segment"); + entries.add(value); + return value; + } + } + }); + } + @Override public void clear() { for (Segment segment : this.segments) { diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index 00d19005774..5a001e74b7b 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -53,7 +53,7 @@ class ConcurrentReferenceHashMapTests { @Test - void shouldCreateWithDefaults() { + void createWithDefaults() { ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(); assertThat(map.getSegmentsSize()).isEqualTo(16); assertThat(map.getSegment(0).getSize()).isEqualTo(1); @@ -61,7 +61,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldCreateWithInitialCapacity() { + void createWithInitialCapacity() { ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(32); assertThat(map.getSegmentsSize()).isEqualTo(16); assertThat(map.getSegment(0).getSize()).isEqualTo(2); @@ -69,7 +69,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldCreateWithInitialCapacityAndLoadFactor() { + void createWithInitialCapacityAndLoadFactor() { ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(32, 0.5f); assertThat(map.getSegmentsSize()).isEqualTo(16); assertThat(map.getSegment(0).getSize()).isEqualTo(2); @@ -77,7 +77,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldCreateWithInitialCapacityAndConcurrentLevel() { + void createWithInitialCapacityAndConcurrentLevel() { ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(16, 2); assertThat(map.getSegmentsSize()).isEqualTo(2); assertThat(map.getSegment(0).getSize()).isEqualTo(8); @@ -85,7 +85,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldCreateFullyCustom() { + void createFullyCustom() { ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(5, 0.5f, 3); // concurrencyLevel of 3 ends up as 4 (nearest power of 2) assertThat(map.getSegmentsSize()).isEqualTo(4); @@ -95,28 +95,28 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldNeedNonNegativeInitialCapacity() { + void nonNegativeInitialCapacity() { assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 1)); assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(-1, 1)) .withMessageContaining("Initial capacity must not be negative"); } @Test - void shouldNeedPositiveLoadFactor() { + void positiveLoadFactor() { assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 0.1f, 1)); assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(0, 0.0f, 1)) .withMessageContaining("Load factor must be positive"); } @Test - void shouldNeedPositiveConcurrencyLevel() { + void positiveConcurrencyLevel() { assertThatNoException().isThrownBy(() -> new ConcurrentReferenceHashMap(1, 1)); assertThatIllegalArgumentException().isThrownBy(() -> new ConcurrentReferenceHashMap(1, 0)) .withMessageContaining("Concurrency level must be positive"); } @Test - void shouldPutAndGet() { + void putAndGet() { // NOTE we are using mock references so we don't need to worry about GC assertThat(this.map).isEmpty(); this.map.put(123, "123"); @@ -129,14 +129,14 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldReplaceOnDoublePut() { + void replaceOnDoublePut() { this.map.put(123, "321"); this.map.put(123, "123"); assertThat(this.map.get(123)).isEqualTo("123"); } @Test - void shouldPutNullKey() { + void putNullKey() { assertThat(this.map.get(null)).isNull(); assertThat(this.map.getOrDefault(null, "456")).isEqualTo("456"); this.map.put(null, "123"); @@ -145,7 +145,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldPutNullValue() { + void putNullValue() { assertThat(this.map.get(123)).isNull(); assertThat(this.map.getOrDefault(123, "456")).isEqualTo("456"); this.map.put(123, "321"); @@ -157,12 +157,12 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetWithNoItems() { + void getWithNoItems() { assertThat(this.map.get(123)).isNull(); } @Test - void shouldApplySupplementalHash() { + void applySupplementalHash() { Integer key = 123; this.map.put(key, "123"); assertThat(this.map.getSupplementalHash()).isNotEqualTo(key.hashCode()); @@ -170,7 +170,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetFollowingNexts() { + void getFollowingNexts() { // Use loadFactor to disable resize this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1); this.map.put(1, "1"); @@ -184,7 +184,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldResize() { + void resize() { this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); this.map.put(1, "1"); assertThat(this.map.getSegment(0).getSize()).isEqualTo(1); @@ -214,7 +214,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldPurgeOnGet() { + void purgeOnGet() { this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); for (int i = 1; i <= 5; i++) { this.map.put(i, String.valueOf(i)); @@ -229,7 +229,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldPurgeOnPut() { + void purgeOnPut() { this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1); for (int i = 1; i <= 5; i++) { this.map.put(i, String.valueOf(i)); @@ -245,28 +245,28 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldPutIfAbsent() { + void putIfAbsent() { assertThat(this.map.putIfAbsent(123, "123")).isNull(); assertThat(this.map.putIfAbsent(123, "123b")).isEqualTo("123"); assertThat(this.map.get(123)).isEqualTo("123"); } @Test - void shouldPutIfAbsentWithNullValue() { + void putIfAbsentWithNullValue() { assertThat(this.map.putIfAbsent(123, null)).isNull(); assertThat(this.map.putIfAbsent(123, "123")).isNull(); assertThat(this.map.get(123)).isNull(); } @Test - void shouldPutIfAbsentWithNullKey() { + void putIfAbsentWithNullKey() { assertThat(this.map.putIfAbsent(null, "123")).isNull(); assertThat(this.map.putIfAbsent(null, "123b")).isEqualTo("123"); assertThat(this.map.get(null)).isEqualTo("123"); } @Test - void shouldRemoveKeyAndValue() { + void removeKeyAndValue() { this.map.put(123, "123"); assertThat(this.map.remove(123, "456")).isFalse(); assertThat(this.map.get(123)).isEqualTo("123"); @@ -276,7 +276,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldRemoveKeyAndValueWithExistingNull() { + void removeKeyAndValueWithExistingNull() { this.map.put(123, null); assertThat(this.map.remove(123, "456")).isFalse(); assertThat(this.map.get(123)).isNull(); @@ -286,7 +286,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldReplaceOldValueWithNewValue() { + void replaceOldValueWithNewValue() { this.map.put(123, "123"); assertThat(this.map.replace(123, "456", "789")).isFalse(); assertThat(this.map.get(123)).isEqualTo("123"); @@ -295,7 +295,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldReplaceOldNullValueWithNewValue() { + void replaceOldNullValueWithNewValue() { this.map.put(123, null); assertThat(this.map.replace(123, "456", "789")).isFalse(); assertThat(this.map.get(123)).isNull(); @@ -304,21 +304,61 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldReplaceValue() { + void replaceValue() { this.map.put(123, "123"); assertThat(this.map.replace(123, "456")).isEqualTo("123"); assertThat(this.map.get(123)).isEqualTo("456"); } @Test - void shouldReplaceNullValue() { + void replaceNullValue() { this.map.put(123, null); assertThat(this.map.replace(123, "456")).isNull(); assertThat(this.map.get(123)).isEqualTo("456"); } @Test - void shouldGetSize() { + void computeIfAbsent() { + assertThat(this.map.computeIfAbsent(123, k -> "123")).isEqualTo("123"); + assertThat(this.map.computeIfAbsent(123, k -> "123b")).isEqualTo("123"); + assertThat(this.map.get(123)).isEqualTo("123"); + this.map.remove(123); + assertThat(this.map.computeIfAbsent(123, k -> null)).isNull(); + assertThat(this.map.containsKey(123)).isFalse(); + } + + @Test + void computeIfPresent() { + assertThat(this.map.computeIfPresent(123, (k, v) -> "123")).isNull(); + this.map.put(123, "123"); + assertThat(this.map.computeIfPresent(123, (k, v) -> v + "b")).isEqualTo("123b"); + assertThat(this.map.get(123)).isEqualTo("123b"); + assertThat(this.map.computeIfPresent(123, (k, v) -> null)).isNull(); + assertThat(this.map.containsKey(123)).isFalse(); + } + + @Test + void compute() { + assertThat(this.map.compute(123, (k, v) -> "123" + v)).isEqualTo("123null"); + assertThat(this.map.compute(123, (k, v) -> null)).isNull(); + assertThat(this.map.compute(123, (k, v) -> null)).isNull(); + assertThat(this.map.compute(123, (k, v) -> "123")).isEqualTo("123"); + assertThat(this.map.compute(123, (k, v) -> v + "b")).isEqualTo("123b"); + assertThat(this.map.get(123)).isEqualTo("123b"); + } + + @Test + void merge() { + assertThat(this.map.merge(123, "123", (v1, v2) -> v1 + v2)).isEqualTo("123"); + assertThat(this.map.merge(123, null, (v1, v2) -> v1 + v2)).isEqualTo("123null"); + assertThat(this.map.merge(123, null, (v1, v2) -> null)).isNull(); + assertThat(this.map.merge(123, "123", (v1, v2) -> v1 + v2)).isEqualTo("123"); + assertThat(this.map.merge(123, "b", (v1, v2) -> v1 + v2)).isEqualTo("123b"); + assertThat(this.map.get(123)).isEqualTo("123b"); + } + + @Test + void size() { assertThat(this.map).isEmpty(); this.map.put(123, "123"); this.map.put(123, null); @@ -327,7 +367,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldSupportIsEmpty() { + void isEmpty() { assertThat(this.map).isEmpty(); this.map.put(123, "123"); this.map.put(123, null); @@ -336,7 +376,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldContainKey() { + void containsKey() { assertThat(this.map.containsKey(123)).isFalse(); assertThat(this.map.containsKey(456)).isFalse(); this.map.put(123, "123"); @@ -346,7 +386,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldContainValue() { + void containsValue() { assertThat(this.map.containsValue("123")).isFalse(); assertThat(this.map.containsValue(null)).isFalse(); this.map.put(123, "123"); @@ -356,7 +396,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldRemoveWhenKeyIsInMap() { + void removeWhenKeyIsInMap() { this.map.put(123, null); this.map.put(456, "456"); this.map.put(null, "789"); @@ -367,14 +407,14 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldRemoveWhenKeyIsNotInMap() { + void removeWhenKeyIsNotInMap() { assertThat(this.map.remove(123)).isNull(); assertThat(this.map.remove(null)).isNull(); assertThat(this.map).isEmpty(); } @Test - void shouldPutAll() { + void putAll() { Map m = new HashMap<>(); m.put(123, "123"); m.put(456, null); @@ -387,7 +427,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldClear() { + void clear() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); @@ -399,7 +439,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetKeySet() { + void keySet() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); @@ -411,7 +451,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetValues() { + void valuesCollection() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); @@ -426,7 +466,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetEntrySet() { + void getEntrySet() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); @@ -438,7 +478,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldGetEntrySetFollowingNext() { + void getEntrySetFollowingNext() { // Use loadFactor to disable resize this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1); this.map.put(1, "1"); @@ -452,7 +492,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldRemoveViaEntrySet() { + void removeViaEntrySet() { this.map.put(1, "1"); this.map.put(2, "2"); this.map.put(3, "3"); @@ -468,7 +508,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldSetViaEntrySet() { + void setViaEntrySet() { this.map.put(1, "1"); this.map.put(2, "2"); this.map.put(3, "3"); @@ -502,7 +542,7 @@ class ConcurrentReferenceHashMapTests { } @Test - void shouldSupportNullReference() { + void supportNullReference() { // GC could happen during restructure so we must be able to create a reference for a null entry map.createReferenceManager().createReference(null, 1234, null); }