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);