基于读操作执行写逻辑时,能否用ConcurrentHashMap替代同步HashMap?
当然可以换,但有个关键坑得提前踩明白——你原来的synchronized(hashMap)块是把整个「检查-然后-操作」的流程变成了原子操作,而ConcurrentHashMap的单个方法(比如get、put)虽然本身是线程安全的,但如果你直接把代码拆成先get再判断执行put,这两步之间会出现并发漏洞,多个线程可能同时钻空子,导致结果不符合预期。
举个例子:线程A和线程B同时调用get(id),都拿到null,然后俩线程一起进入else分支执行添加操作——如果是给同一个id加值,那后执行的线程会覆盖前一个的结果;如果是加不同的键值对,虽然这两个put本身没问题,但如果你的业务要求「只有当id不存在时,才必须添加那个额外的键值对」,那这种分开的操作就没法保证这个逻辑的原子性了。
那正确的替换姿势是什么?分两种情况说:
情况1:else分支是给同一个id添加值
如果你的逻辑是「id存在就更新它的值,不存在就插入id的初始值」,直接用ConcurrentHashMap的compute方法就行,它能把整个判断和操作变成原子动作:
concurrentHashMap.compute(id, (key, existingValue) -> { if (existingValue != null) { // 这里写你的更新逻辑,返回更新后的值 return "更新后的新值"; } else { // 这里写插入初始值的逻辑,返回初始值 return "初始值"; } });
compute方法在执行lambda里的逻辑时,会锁定当前key对应的节点(JDK8+的实现),锁的粒度比原来整个map小得多,并发性能自然就上去了,而且完全保证逻辑的原子性。
情况2:else分支是添加另一个不同的键值对
如果你的逻辑是「id不存在时,就插入另一个完全不同的key(比如otherId)和对应的值」,这时候因为涉及两个不同的key操作,ConcurrentHashMap的单个方法没法保证这两个操作的原子性。这时候你得评估业务需求:
- 如果业务要求这两个操作必须同时成功/失败(原子性),那你可以给这个逻辑块加一个专门的锁对象(别锁整个map),这样其他不涉及这两个key的操作依然能并发执行,比原来的性能还是要好;
- 如果业务允许这两个操作分开执行(比如哪怕id被其他线程插入了,这个额外的键值对依然可以添加),那直接分开调用
put就行,但要确认逻辑的正确性。
另外补充一句:如果你的更新操作只是简单的替换值,也可以用putIfAbsent配合后续操作,但compute更适合处理复杂的分支逻辑。
总的来说,替换ConcurrentHashMap完全没问题,而且能显著提升并发性能——核心就是别把原来的原子逻辑拆成零散的方法调用,要用ConcurrentHashMap提供的原子方法把整个流程封装起来。
内容的提问来源于stack exchange,提问作者user9586276

