问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

...| 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

发布网友 发布时间:2024-09-25 17:58

我来回答

1个回答

热心网友 时间:2024-09-29 21:21

前言

我们先看一段代码,代码中使用 Map 的时候,有可能会这么写:

Map<String,?Value>?map;//?...Value?result?=?map.get(key);if?(null?==?result)?{????result?=?this.calculateValue(key);????map.put(key,?result);}return?result;

Java 8 的 java.util.Map 里面有个方法 computeIfAbsent,能够简化以上代码:

Map<String,?Value>?map;//?...return?map.computeIfAbsent(key,?this::calculateValue);

以上这种写法除了简洁,如果使用的是 java.util.concurrent.ConcurrentHashMap,还能够在并发调用的情况下确保 calculateValue 方法不会被重复调用,保证原子性。

不过,前段时间对 Apache ShardingSphere-Proxy 做压测时遇到一个问题,当 BenchmarkSQL 连接 ShardingSphere Proxy 的 Terminal 数量比较高时,其中一条很简单的插入 SQL 执行延迟增加了很多。借助 Async Profiler 发现 Java 8 ConcurrentHashMap 的 computeIfAbsent 在性能上有坑。

不了解 Apache ShardingSphere 的读者可以参考 https://github.com/apache/shardingsphere。

排查

考虑到当时的压测的现象是 BenchmarkSQL 并发数(Terminals)越高,New Order 业务中一条简单且重复执行的 insert SQL 执行延时越长。但是 ShardingSphere-Proxy 的所在机器的 CPU 也没有压满,考虑是不是 Proxy 代码层面存在瓶颈,于是借助 async-profiler 对压测状态下的 Proxy JVM 采样。

./profiler.sh?-e?lock?--lock?1ms?-d?180?-o?jfr?-f?output.jfr?$PID

关于 async-profiler 可以参考 https://github.com/jvm-profiling-tools/async-profiler,后续我也考虑写一些相关文章。

使用 IDEA 读取采样获得的 jfr 文件,看到 Java Monitor Blocked 事件居然有三百多万次!根据堆栈,找到 ShardingSphere 这段使用了 computeIfAbsent 代码,以下为节选:

????//?...????private?static?final?Map<String,?SQLExecutionUnitBuilder>?TYPE_TO_BUILDER_MAP?=?new?ConcurrentHashMap<>(8,?1);????//?...????public?DriverExecutionPrepareEngine(final?String?type,?final?int?maxConnectionsSizePerQuery,?final?ExecutorDriverManager<C,??,??>?executorDriverManager,?????????????????????????????????????????final?StorageResourceOption?option,?final?Collection<ShardingSphereRule>?rules)?{????????super(maxConnectionsSizePerQuery,?rules);????????this.executorDriverManager?=?executorDriverManager;????????this.option?=?option;????????sqlExecutionUnitBuilder?=?TYPE_TO_BUILDER_MAP.computeIfAbsent(type,?????????????key?->?TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class,?key,?new?Properties()));????}????//?...

https://github.com/apache/shardingsphere/blob/3b840b339ac580a7247b866f5904c514b169065f/shardingsphere-infra/shardingsphere-infra-executor/src/main/java/org/apache/shardingsphere/infra/executor/sql/prepare/driver/DriverExecutionPrepareEngine.java#L65

以上这段代码在每一次 Proxy 与数据库交互前都会执行,即通过 Proxy 执行 CRUD 操作的必经之路,而且里面的 type 目前只有 2 种,分别是 JDBC.STATEMENT 和 JDBC.PREPARED_STATEMENT,所以在高并发的情况下会有大量的线程调用同一个 key 的 computeIfAbsent。

我的理解是,如果在 key 存在的情况下,computeIfAbsent 操作就不存在修改的情况了,直接 get 出来就好,那事实如何? 看一下 computeIfAbsent 方法的实现(JDK 是 Oracle 8u311),节选代码并加了一些注释:

????public?V?computeIfAbsent(K?key,?Function<??super?K,???extends?V>?mappingFunction)?{????????if?(key?==?null?||?mappingFunction?==?null)????????????throw?new?NullPointerException();????????int?h?=?spread(key.hashCode());????????V?val?=?null;????????int?binCount?=?0;????????for?(Node<K,V>[]?tab?=?table;;)?{????????????Node<K,V>?f;?int?n,?i,?fh;????????????if?(tab?==?null?||?(n?=?tab.length)?==?0)????????????????//?Map?初始化????????????????tab?=?initTable();????????????else?if?((f?=?tabAt(tab,?i?=?(n?-?1)?&?h))?==?null)?{????????????????//?key?不存在且?hash?对应的位置还没有东西????????????????Node<K,V>?r?=?new?ReservationNode<K,V>();????????????????synchronized?(r)?{????????????????????//?初始化?hash?对应的位置,放入?kv?等操作????????????????}????????????}????????????else?if?((fh?=?f.hash)?==?MOVED)????????????????//?Map?正忙着扩容????????????????tab?=?helpTransfer(tab,?f);????????????else?{????????????????//?key?的?hash?对应的位置已经存在链表或红黑树????????????????boolean?added?=?false;????????????????synchronized?(f)?{????????????????????if?(tabAt(tab,?i)?==?f)?{????????????????????????if?(fh?>=?0)?{????????????????????????????//?去链表里面找?key????????????????????????}????????????????????????else?if?(f?instanceof?TreeBin)?{????????????????????????????//?去红黑树里面找?key????????????????????????}????????????????????}????????????????}????????????????//?省略部分代码????????????}????????}????????//?省略部分代码????????return?val;????}

根据我对源码的理解,即使 key 存在,computeIfAbsent 去找 key 的时候,都会进入 synchronized 代码。 那这相比 ConcurrentHashMap 不加锁的 get 操作不就影响性能了吗?Google 一下相应的话题,发现了一些内容: https://bugs.openjdk.java.net/browse/JDK-8161372 这个问题早就有人提过了,也在 JDK 9 处理了。截至本文编写 JDK 17 已经正式发布了。

解决

在目前 JDK 8 仍然盛行的环境下,我们有必要考虑如何避免上面的问题,于是相应的处理方法就诞生了:https://github.com/apache/shardingsphere/pull/13275/files

SQLExecutionUnitBuilder?result;if?(null?==?(result?=?TYPE_TO_BUILDER_MAP.get(type)))?{????result?=?TYPE_TO_BUILDER_MAP.computeIfAbsent(type,?key?->?TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class,?key,?new?Properties()));}return?result;

https://github.com/apache/shardingsphere/blob/300cbe86cf3d837a925c68c9babcec4839a2078f/shardingsphere-infra/shardingsphere-infra-executor/src/main/java/org/apache/shardingsphere/infra/executor/sql/prepare/driver/DriverExecutionPrepareEngine.java#L76-L80

每次从 Map 中获取 value 前,都先用 get 做一次检查,value 不存在才使用 computeIfAbsent 放入 value。由于 ConcurrentHashMap 的 computeIfAbsent 可以保证操作原子性,这里也不需要自己加 synchronized 或者做多重检查之类的操作。

问题解决~

附:JMH 测试测试环境测试代码package?icu.wwj.jmh.dangling;import?org.openjdk.jmh.annotations.Benchmark;import?org.openjdk.jmh.annotations.Fork;import?org.openjdk.jmh.annotations.Level;import?org.openjdk.jmh.annotations.Measurement;import?org.openjdk.jmh.annotations.Scope;import?org.openjdk.jmh.annotations.Setup;import?org.openjdk.jmh.annotations.State;import?org.openjdk.jmh.annotations.Threads;import?org.openjdk.jmh.annotations.Warmup;import?java.util.Map;import?java.util.concurrent.ConcurrentHashMap;@Fork(3)@Warmup(iterations?=?3,?time?=?5)@Measurement(iterations?=?3,?time?=?5)@Threads(16)@State(Scope.Benchmark)public?class?ConcurrentHashMapBenchmark?{????private?static?final?String?KEY?=?"key";????private?static?final?Object?VALUE?=?new?Object();????private?final?Map<String,?Object>?concurrentMap?=?new?ConcurrentHashMap<>(1,?1);????@Setup(Level.Iteration)????public?void?setup()?{????????concurrentMap.clear();????}????@Benchmark????public?Object?benchGetBeforeComputeIfAbsent()?{????????Object?result?=?concurrentMap.get(KEY);????????if?(null?==?result)?{????????????result?=?concurrentMap.computeIfAbsent(KEY,?__?->?VALUE);????????}????????return?result;????}????@Benchmark????public?Object?benchComputeIfAbsent()?{????????return?concurrentMap.computeIfAbsent(KEY,?__?->?VALUE);????}}JDK 8 测试结果#?JMH?version:?1.33#?VM?version:?JDK?1.8.0_311,?Java?HotSpot(TM)?64-Bit?Server?VM,?25.311-b11#?VM?invoker:?/usr/local/java/jdk1.8.0_311/jre/bin/java#?VM?options:?-Dvisualvm.id=172855224679674?-javaagent:/home/wuweijie/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/213.5744.223/lib/idea_rt.jar=38763:/home/wuweijie/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/213.5744.223/bin?-Dfile.encoding=UTF-8#?Blackhole?mode:?full?+?dont-inline?hint?(default,?use?-Djmh.blackhole.autoDetect=true?to?auto-detect)#?Warmup:?3?iterations,?5?s?each#?Measurement:?3?iterations,?5?s?each#?Timeout:?10?min?per?iteration#?Threads:?16?threads,?will?synchronize?iterations#?Benchmark?mode:?Throughput,?ops/time#?Benchmark:?icu.wwj.jmh.dangling.ConcurrentHashMapBenchmark.benchComputeIfAbsent#?Run?progress:?0.00%?complete,?ETA?00:03:00#?Fork:?1?of?3#?Warmup?Iteration???1:?11173878.242?ops/s#?Warmup?Iteration???2:?8471364.065?ops/s#?Warmup?Iteration???3:?8766401.960?ops/sIteration???1:?8776260.796?ops/sIteration???2:?8632907.974?ops/sIteration???3:?8557264.788?ops/s#?Run?progress:?16.67%?complete,?ETA?00:02:33#?Fork:?2?of?3#?Warmup?Iteration???1:?7757506.431?ops/s#?Warmup?Iteration???2:?8176991.807?ops/s#?Warmup?Iteration???3:?8795107.589?ops/sIteration???1:?8668883.337?ops/sIteration???2:?8866318.073?ops/sIteration???3:?8848517.540?ops/s#?Run?progress:?33.33%?complete,?ETA?00:02:02#?Fork:?3?of?3#?Warmup?Iteration???1:?8154698.571?ops/s#?Warmup?Iteration???2:?8317945.491?ops/s#?Warmup?Iteration???3:?8884286.732?ops/sIteration???1:?8912555.062?ops/sIteration???2:?8894750.001?ops/sIteration???3:?8780504.227?ops/sResult?"icu.wwj.jmh.dangling.ConcurrentHashMapBenchmark.benchComputeIfAbsent":??8770884.644?±(99.9%)?210678.797?ops/s?[Average]??(min,?avg,?max)?=?(8557264.788,?8770884.644,?8912555.062),?stdev?=?125371.573??CI?(99.9%):?[8560205.847,?8981563.442]?(assumes?normal?distribution)#?JMH?version:?1.33#?VM?version:?JDK?1.8.0_311,?Java?HotSpot(TM)?64-Bit?Server?VM,?25.311-b11#?VM?invoker:?/usr/local/java/jdk1.8.0_311/jre/bin/java#?VM?options:?-Dvisualvm.id=172855224679674?-javaagent:/home/wuweijie/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/213.5744.223/lib/idea_rt.jar=38763:/home/wuweijie/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/213.5744.223/bin?-Dfile.encoding=UTF-8#?Blackhole?mode:?full?+?dont-inline?hint?(default,?use?-Djmh.blackhole.autoDetect=true?to?auto-detect)#?Warmup:?3?iterations,?5?s?each#?Measurement:?3?iterations,?5?s?each#?Timeout:?10?min?per?iteration#?Threads:?16?threads,?will?synchronize?iterations#?Benchmark?mode:?Throughput,?ops/time#?Benchmark:?icu.wwj.jmh.dangling.ConcurrentHashMapBenchmark.benchGetBeforeComputeIfAbsent#?Run?progress:?50.00%?complete,?ETA?00:01:31#?Fork:?1?of?3#?Warmup?Iteration???1:?1881091972.510?ops/s#?Warmup?Iteration???2:?1843432746.197?ops/s#?Warmup?Iteration???3:?2353506882.860?ops/sIteration???1:?2389458285.091?ops/sIteration???2:?2391001171.657?ops/sIteration???3:?2387181602.010?ops/s#?Run?progress:?66.67%?complete,?ETA?00:01:01#?Fork:?2?of?3#?Warmup?Iteration???1:?1872514017.315?ops/s#?Warmup?Iteration???2:?1855584197.510?ops/s#?Warmup?Iteration???3:?2342392977.207?ops/sIteration???1:?2378551289.692?ops/sIteration???2:?2374081014.168?ops/sIteration???3:?2389909613.865?ops/s#?Run?progress:?83.33%?complete,?ETA?00:00:30#?Fork:?3?of?3#?Warmup?Iteration???1:?1880210774.729?ops/s#?Warmup?Iteration???2:?1804266170.900?ops/s#?Warmup?Iteration???3:?2337740394.373?ops/sIteration???1:?2363741084.192?ops/sIteration???2:?2372565304.724?ops/sIteration???3:?2388015878.515?ops/sResult?"icu.wwj.jmh.dangling.C
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
...时间会发作,很痛的,请问是这样吗?会痛多久? ...已经坏了,如今全身浮肿,尿排不出,医生说就这 我爷爷得了肝癌晚期,腹水,不能手术.听说氩氦超冷刀技术能治.哪里有... 父亲59岁得了肝癌晚期,医生说最多半年时间了,上面还有80多岁的爷爷... 长春跑大连的列车员,怎么区分正式分和临时工呢? 员工刚怀孕就被开除,开除负责人拒不认错,你如何看待该公司的态度?_百 ... 员工在职期间怀孕,老板可以无故迟退吗? 老板开除怀孕女员工违法吗 如何选购蚊帐 什么样的蚊帐最好 Jean used to be crazy ___ computei games,but now she is interested... 业务常见error示例——并发工具类库导致的线程安全问题 爱情风暴美丽99中的高捷是真的爱天丽吗? 电视剧爱情风暴美丽99是根据什么小说改编 求问这一幕发生在爱情风暴美丽99的第几集?(除了最后一集) 爱情风暴美丽99电视原声带创作背景 被子上有螨虫会出现什么症状 被子螨虫怎么去除最有效最彻底 螨虫的危害竟然如此的大!教你如何赶走螨虫 一次性筷子是属于什么垃圾 C1驾驶证一周期先扣11分再扣12分是怎么回事? c1驾照考试年龄要求.急急!!! 无机房电梯厂家哪家质量好 小型无机房电梯哪家的好? ...用脱毛刀刮了2次手毛,但是每次一刮一个星期后就长出来了。。。_百度... ...那时侯也不懂,就用刮胡刀挂了以后毛就长的又黑有粗了.该怎么办啊... 海信电视显示智能电视系统启动中,是什么意思? 做鼻息肉手术大概要多少钱?手术以后还会复发吗? 鼻息肉手术多少钱,鼻息肉手术疼不疼? 鼻息肉怎么治疗,多少钱? 您好: 请问医生,做一个鼻息肉手术要多少钱? 武汉家庭月收到3800怎么理财 ...但是没有卡, 如何在武汉存钱 北京取,不要手续费 在武汉一个月2000块钱够花吗?如果有住的地方的话,再开两千块钱能够用... 自制发面包子的做法(自发面怎么做包子) 作文《孙悟空求医》450字 想象文 孙悟空当医生 作文 描写孙悟空的作文 450字 有哪些口碑不错的淘宝零食店铺推荐? 明星不拍戏去乡下养鸡,下的鸡蛋半个娱乐圈的人都在吃,现在怎样? 多么有趣的吃早餐的时间呀用英语咋说 ...用我的名字,头像在玩朋友网。怎么样才能注销那个号码 三叔在蛇着鬼城中有没有死啊?怎么后来吴邪还假扮三叔整顿盘口啊?谁能告... 2022年凌源市中考总分多少 《美丽99》结局是什么?有第二部吗?女主角的真实姓名? 帮忙取下英文名字 我叫张成 男的 谢谢! 笔记本被小摔了下,开机显示器颜色变绿色了,开始花屏怎么回事? 笔记本电脑被摔了一下然后电脑屏幕就出现花纹,看不见桌面怎么回事 笔记本电脑被砸了下,屏幕就花了,怎么办? 鄂尔多斯农村商业银行股份有限公司育英支行怎么样? 内蒙古伊金霍洛农村商业银行股份有限公司中汇支行怎么样?