如何向ConcurrentHashMap中原子性地条件更新值?
游戏最高分的ConcurrentHashMap原子更新问题
问题背景
我维护着一个存储不同游戏最高分的映射表,收到新分数时要判断是否高于当前最高分,是的话就更新成新最高分。
选了ConcurrentHashMap来存,但发现原子性检查+条件更新的实现有点麻烦:
- 试了
compute方法,本来想返回null保留原条目,结果直接把条目删了(示例代码和运行结果如下)。 - 如果返回当前值,又怕触发没必要的更新,毕竟ConcurrentHashMap的优势就是高频读取能并发,只有实际更新时才同步、建立先行关系。
示例代码:
import java.util.concurrent.ConcurrentHashMap; public class ConcHashMapTest{ public static void main(String[] args){ ConcurrentHashMap<String,String> h = new ConcurrentHashMap<>(); h.put("TEST","value1"); /* 问题是,能不能原子性地检查值,只在需要的时候更新? 我知道可以用compute,但必须要修改Map吗?返回null能不能保留原映射? */ h.compute("TEST",(k,v) -> { if (v.equals("changeme")){ return "CHANGED"; } else { return null; } }); System.out.println("Now hashmap is "+h); } }
运行结果:
Now hashmap is {}
后来的思考:
- 搞清楚了ConcurrentHashMap的优势是高频读取时的并发能力,要原子更新就得同步,但
compute只会锁对应的桶,不是整个Map,那返回原值是不是正确做法? - 最后我倾向于不追求原子性了:每秒几千个分数里真正的高分极少,两个高分同时到导致代码交错的概率几乎为零,就用下面这段代码,接受理论上可能漏记最高分的漏洞:
if (score > map.get("SCORE")){ map.put("SCORE",score); }
可行解决方案
1. 正确用compute实现原子更新
不需要更新的时候直接返回当前值v就行,ConcurrentHashMap内部会判断返回值和原值是否相等,只有真的变了才会执行更新、建立先行关系,不会有多余操作。
针对最高分场景的代码:
map.compute(gameId, (k, currentScore) -> { // 处理游戏第一次提交分数的情况(key不存在) if (currentScore == null) { return newScore; } // 新分数更高就更新,否则返回原值 return newScore > currentScore ? newScore : currentScore; });
这种方式完全保证原子性,只有新分数确实更高时才会触发实际更新,不影响并发读取的性能。
2. 用merge更简洁地实现原子更新
如果你的场景是“只有新值满足条件才替换旧值”,merge更合适——key不存在就插入新值,存在的话就用传入的函数计算结果。
最高分场景的代码:
// 第三个参数是BiFunction,key存在时用旧值和新值计算最终结果 map.merge(gameId, newScore, (oldScore, newScore) -> newScore > oldScore ? newScore : oldScore );
merge也是原子操作,内部只锁对应的桶,不会影响整个Map的并发读取。
3. 非原子方案的风险说明
你说的先get再put的非原子方案,确实有极小概率的竞态问题:比如两个线程同时读到同一个旧最高分,然后都执行put,最后可能是较低的那个高分覆盖了真正的最高分。
如果业务能接受这种理论上的极小概率错误,这个方案确实最简单高效,完全没锁操作,读写都快。
内容的提问来源于stack exchange,提问作者Bruce
相关产品推荐
相关产品推荐

