简介
最近在看dubbo源码,然后查看git日志发现有一个有趣的提交dubbo issues,这个pr修复了JDK8中ConcurrentHashMap#computeIfAbsent存在的性能问题。
问题
接下来进行基准测试,测试JDK版本分别为8和11,比较直接实用ConcurrentHashMap#computeIfAbsent和先判空如果为空再进行ConcurrentHashMap#computeIfAbsent。
基准测试代码
预热迭代5次,每次1秒,开启10个线程
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
@Fork(1)
public class ConcurrentHashMapBenchmark {
private static final String KEY = "test";
private final Map<String, Object> concurrentMap = new ConcurrentHashMap<>();
@Setup(Level.Iteration)
public void setup() {
concurrentMap.clear();
}
@Benchmark
@Threads(16)
public Object computeIfAbsentIfNull() {
Object result = concurrentMap.get(KEY);
if (null == result) {
result = concurrentMap.computeIfAbsent(KEY, key -> 1);
}
return result;
}
@Benchmark
@Threads(16)
public Object computeIfAbsent() {
return concurrentMap.computeIfAbsent(KEY, key -> 1);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ConcurrentHashMapBenchmark.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
}
Java8 测试结果
# JMH version: 1.35
# VM version: JDK 1.8.0_341, Java HotSpot(TM) 64-Bit Server VM, 25.341-b10
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62618:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.common.ConcurrentHashMapBenchmark.computeIfAbsentIfNull
# Run progress: 0.00% complete, ETA 00:00:20
# Fork: 1 of 1
# Warmup Iteration 1: 1216169200.120 ops/s
# Warmup Iteration 2: 796866117.384 ops/s
# Warmup Iteration 3: 1301323849.345 ops/s
# Warmup Iteration 4: 1384824524.282 ops/s
# Warmup Iteration 5: 1339219905.768 ops/s
Iteration 1: 1288413287.614 ops/s
Iteration 2: 1389470519.744 ops/s
Iteration 3: 1304004526.179 ops/s
Iteration 4: 1432687506.799 ops/s
Iteration 5: 1334467628.016 ops/s
Result "com.common.ConcurrentHashMapBenchmark.computeIfAbsentIfNull":
1349808693.670 ±(99.9%) 232196734.840 ops/s [Average]
(min, avg, max) = (1288413287.614, 1349808693.670, 1432687506.799), stdev = 60300754.665
CI (99.9%): [1117611958.831, 1582005428.510] (assumes normal distribution)
# JMH version: 1.35
# VM version: JDK 1.8.0_341, Java HotSpot(TM) 64-Bit Server VM, 25.341-b10
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_341.jdk/Contents/Home/jre/bin/java
# VM options: -javaagent:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62618:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.common.ConcurrentHashMapBenchmark.omputeIfAbsent
# Run progress: 50.00% complete, ETA 00:00:11
# Fork: 1 of 1
# Warmup Iteration 1: 30656957.524 ops/s
# Warmup Iteration 2: 28775930.354 ops/s
# Warmup Iteration 3: 30972353.191 ops/s
# Warmup Iteration 4: 34015742.902 ops/s
# Warmup Iteration 5: 34973423.646 ops/s
Iteration 1: 31921259.973 ops/s
Iteration 2: 31575005.160 ops/s
Iteration 3: 30769995.069 ops/s
Iteration 4: 31608357.784 ops/s
Iteration 5: 33258698.519 ops/s
Result "com.common.ConcurrentHashMapBenchmark.omputeIfAbsent":
31826663.301 ±(99.9%) 3490736.826 ops/s [Average]
(min, avg, max) = (30769995.069, 31826663.301, 33258698.519), stdev = 906533.268
CI (99.9%): [28335926.475, 35317400.127] (assumes normal distribution)
# Run complete. Total time: 00:00:22
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
ConcurrentHashMapBenchmark.computeIfAbsentIfNull thrpt 5 1349808693.670 ± 232196734.840 ops/s
ConcurrentHashMapBenchmark.computeIfAbsent thrpt 5 31826663.301 ± 3490736.826 ops/s
使用JDK8,两种方式吞吐量差别巨大
Java11 测试结果
# JMH version: 1.35
# VM version: JDK 11.0.16.1, Java HotSpot(TM) 64-Bit Server VM, 11.0.16.1+1-LTS-1
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-11.0.16.1.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62682:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.common.ConcurrentHashMapBenchmark.computeIfAbsentIfNull
# Run progress: 0.00% complete, ETA 00:00:20
# Fork: 1 of 1
# Warmup Iteration 1: 1067732310.473 ops/s
# Warmup Iteration 2: 1106351110.025 ops/s
# Warmup Iteration 3: 1296937798.110 ops/s
# Warmup Iteration 4: 1326307765.775 ops/s
# Warmup Iteration 5: 1262019848.302 ops/s
Iteration 1: 1198789527.458 ops/s
Iteration 2: 1314224000.011 ops/s
Iteration 3: 1311441647.113 ops/s
Iteration 4: 1332859675.583 ops/s
Iteration 5: 1194762511.453 ops/s
Result "com.common.ConcurrentHashMapBenchmark.computeIfAbsentIfNull":
1270415472.324 ±(99.9%) 260845665.876 ops/s [Average]
(min, avg, max) = (1194762511.453, 1270415472.324, 1332859675.583), stdev = 67740791.077
CI (99.9%): [1009569806.448, 1531261138.200] (assumes normal distribution)
# JMH version: 1.35
# VM version: JDK 11.0.16.1, Java HotSpot(TM) 64-Bit Server VM, 11.0.16.1+1-LTS-1
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-11.0.16.1.jdk/Contents/Home/bin/java
# VM options: -javaagent:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62682:/Users/kamtohung/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.8617.56/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.common.ConcurrentHashMapBenchmark.computeIfAbsent
# Run progress: 50.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 1257526551.072 ops/s
# Warmup Iteration 2: 1206044836.430 ops/s
# Warmup Iteration 3: 1332548183.906 ops/s
# Warmup Iteration 4: 1329742866.881 ops/s
# Warmup Iteration 5: 1305133142.912 ops/s
Iteration 1: 1341481353.960 ops/s
Iteration 2: 1261243963.000 ops/s
Iteration 3: 1297349960.836 ops/s
Iteration 4: 1237097809.005 ops/s
Iteration 5: 953985125.952 ops/s
Result "com.common.ConcurrentHashMapBenchmark.computeIfAbsent":
1218231642.550 ±(99.9%) 588665146.024 ops/s [Average]
(min, avg, max) = (953985125.952, 1218231642.550, 1341481353.960), stdev = 152874469.036
CI (99.9%): [629566496.526, 1806896788.575] (assumes normal distribution)
# Run complete. Total time: 00:00:21
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
ConcurrentHashMapBenchmark.computeIfAbsentIfNull thrpt 5 1270415472.324 ± 260845665.876 ops/s
ConcurrentHashMapBenchmark.computeIfAbsent thrpt 5 1218231642.550 ± 588665146.024 ops/s
使用JDK11,两种方式吞吐量差距不大
JDK8和JDK11 computeIfAbsent不同之处
可以看到,JDK11用了这段代码解决此问题
else if (fh == h // check first node without acquiring lock
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
先判断首个节点是否相等,如果相等则返回,这样可以防止进入下一个判断(下一个判断加了synchronized)。因为在散列表中,hash冲突机率是有的但是一般情况下会很少,所以就只检查了第一个节点是否相等就能解决大部分情况下出现的这种性能问题。而先通过get再进行判空做后续处理的做法需要找到具体是否存在,可能存在遍历链表/红黑树耗时,各有各的好处。
至于下面还有两处修改的地方,是解决了ConcurrentHahsMap#computeIfAbsent死循环的问题,下次在和大家分享下,大家可以看JDK-8062841了解下。
throw new IllegalStateException("Recursive update");
结论
通过了基准测试,我们可以发现在JDK8中ConcurrentHashMap#computeIfAbsent确实存在性能问题,JDK-8161372此问题2016年发现,直到JDK9才正式修复。因为我们通常还是在使用JDK8,所以我们通过先获取一遍如果发现为空再执行computeIfAbsent来解决此BUG。
例子
public Object computeIfAbsentIfNull() {
Object result = concurrentMap.get(KEY);
if (null == result) {
result = concurrentMap.computeIfAbsent(KEY, key -> 1);
}
return result;
}
评论区