diff --git a/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java b/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java
new file mode 100644
index 00000000000..31302a2b1b8
--- /dev/null
+++ b/spring-core/src/jmh/java/org/springframework/util/ConcurrentReferenceHashMapBenchmark.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.util;
+
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.WeakHashMap;
+import java.util.function.Function;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks for {@link ConcurrentReferenceHashMap}.
+ *
This benchmark ensures that {@link ConcurrentReferenceHashMap} performs
+ * better than {@link java.util.Collections#synchronizedMap(Map)} with
+ * concurrent read operations.
+ *
Typically this can be run with {@code "java -jar spring-core-jmh.jar -t 30 -f 2 ConcurrentReferenceHashMapBenchmark"}.
+ * @author Brian Clozel
+ */
+@BenchmarkMode(Mode.Throughput)
+public class ConcurrentReferenceHashMapBenchmark {
+
+ @Benchmark
+ public void concurrentMap(ConcurrentMapBenchmarkData data, Blackhole bh) {
+ for (String element : data.elements) {
+ WeakReference value = data.map.get(element);
+ bh.consume(value);
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class ConcurrentMapBenchmarkData {
+
+ @Param({"500"})
+ public int capacity;
+ private final Function generator = key -> key + "value";
+
+ public List elements;
+
+ public Map> map;
+
+ @Setup(Level.Iteration)
+ public void setup() {
+ this.elements = new ArrayList<>(this.capacity);
+ this.map = new ConcurrentReferenceHashMap<>();
+ Random random = new Random();
+ random.ints(this.capacity).forEach(value -> {
+ String element = String.valueOf(value);
+ this.elements.add(element);
+ this.map.put(element, new WeakReference<>(this.generator.apply(element)));
+ });
+ this.elements.sort(String::compareTo);
+ }
+ }
+
+ @Benchmark
+ public void synchronizedMap(SynchronizedMapBenchmarkData data, Blackhole bh) {
+ for (String element : data.elements) {
+ WeakReference value = data.map.get(element);
+ bh.consume(value);
+ }
+ }
+
+ @State(Scope.Benchmark)
+ public static class SynchronizedMapBenchmarkData {
+
+ @Param({"500"})
+ public int capacity;
+
+ private Function generator = key -> key + "value";
+
+ public List elements;
+
+ public Map> map;
+
+
+ @Setup(Level.Iteration)
+ public void setup() {
+ this.elements = new ArrayList<>(this.capacity);
+ this.map = Collections.synchronizedMap(new WeakHashMap<>());
+ Random random = new Random();
+ random.ints(this.capacity).forEach(value -> {
+ String element = String.valueOf(value);
+ this.elements.add(element);
+ this.map.put(element, new WeakReference<>(this.generator.apply(element)));
+ });
+ this.elements.sort(String::compareTo);
+ }
+ }
+
+}
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 75b084713cc..bc4c3414958 100644
--- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java
+++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java
@@ -16,9 +16,7 @@
package org.springframework.util;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@@ -27,9 +25,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.WeakHashMap;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
@@ -507,66 +503,12 @@ class ConcurrentReferenceHashMapTests {
copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse());
}
- @Test
- @Disabled("Intended for use during development only")
- void shouldBeFasterThanSynchronizedMap() throws InterruptedException {
- Map> synchronizedMap = Collections.synchronizedMap(new WeakHashMap>());
- StopWatch mapTime = timeMultiThreaded("SynchronizedMap", synchronizedMap, v -> new WeakReference<>(String.valueOf(v)));
- System.out.println(mapTime.prettyPrint());
-
- this.map.setDisableTestHooks(true);
- StopWatch cacheTime = timeMultiThreaded("WeakConcurrentCache", this.map, String::valueOf);
- System.out.println(cacheTime.prettyPrint());
-
- // We should be at least 4 time faster
- assertThat(cacheTime.getTotalTimeSeconds()).isLessThan(mapTime.getTotalTimeSeconds() / 4.0);
- }
-
@Test
void shouldSupportNullReference() {
// GC could happen during restructure so we must be able to create a reference for a null entry
map.createReferenceManager().createReference(null, 1234, null);
}
- /**
- * Time a multi-threaded access to a cache.
- * @return the timing stopwatch
- */
- private StopWatch timeMultiThreaded(String id, final Map map,
- ValueFactory factory) throws InterruptedException {
-
- StopWatch stopWatch = new StopWatch(id);
- for (int i = 0; i < 500; i++) {
- map.put(i, factory.newValue(i));
- }
- Thread[] threads = new Thread[30];
- stopWatch.start("Running threads");
- for (int threadIndex = 0; threadIndex < threads.length; threadIndex++) {
- threads[threadIndex] = new Thread("Cache access thread " + threadIndex) {
- @Override
- public void run() {
- for (int j = 0; j < 1000; j++) {
- for (int i = 0; i < 1000; i++) {
- map.get(i);
- }
- }
- }
- };
- }
- for (Thread thread : threads) {
- thread.start();
- }
-
- for (Thread thread : threads) {
- if (thread.isAlive()) {
- thread.join(2000);
- }
- }
- stopWatch.stop();
- return stopWatch;
- }
-
-
private interface ValueFactory {
V newValue(int k);