ConcurrentHashMap的get/put操作原子性问题排查与修复
问题:ConcurrentHashMap多线程操作出现计数异常
在多线程环境下对ConcurrentHashMap执行get和put操作时,出现了不符合预期的计数结果。以下是相关代码和输出:
测试代码
import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Test { private static final List<String> bars = Arrays.asList("1","2","3","4","5","6","7","8","9","10"); private static final String KEY = UUID.randomUUID().toString(); private static ExecutorService executorService = null; public static void main(String[] args) { for (int i = 1; i <= 20; i++) { executorService = Executors.newFixedThreadPool(2); Map<String, AtomicInteger> map = new ConcurrentHashMap<>(); performMapOps(map); executorService.shutdown(); try { while (!executorService.awaitTermination(1, TimeUnit.SECONDS)); System.out.println(map.get(KEY)); } catch (Exception e) { e.printStackTrace(); } } } private static void performMapOps(Map<String, AtomicInteger> map) { for (int i = 1; i <= bars.size(); i++) { executorService.execute(() -> ops(map)); } } private static void ops(Map<String, AtomicInteger> map) { if (!map.containsKey(KEY)) { AtomicInteger atomicInteger = new AtomicInteger(1); map.put(KEY, atomicInteger); } else { map.get(KEY).set(map.get(KEY).intValue() + 1); } } }
预期与实际输出
预期每次输出都是10,但实际运行时偶尔会出现9,示例输出:
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 10 10
异常原因分析
- 初始化阶段的竞态条件:
containsKey和put是两个独立方法,ConcurrentHashMap仅保证单个方法的线程安全,但组合操作不具备原子性。当多个线程同时判断!map.containsKey(KEY)为真时,都会创建新的AtomicInteger(1)并执行put,后续线程的put会覆盖前面线程放入的实例,导致初始计数被重置,最终总计数少一次。 - 计数更新的非原子操作:else分支中
map.get(KEY).set(map.get(KEY).intValue() + 1)存在问题:intValue()取值、加1、set赋值这三步并非原子操作,多线程同时执行时可能出现值被覆盖的情况,也会导致计数不准确。
修复方案
利用ConcurrentHashMap的原子性组合方法,结合AtomicInteger的原子自增操作,解决竞态问题:
修改后的ops方法
private static void ops(Map<String, AtomicInteger> map) { // 原子性完成:若KEY不存在则创建AtomicInteger(0)放入,否则返回已有值 AtomicInteger counter = map.computeIfAbsent(KEY, k -> new AtomicInteger(0)); // 原子性自增1,替代非原子的get+set操作 counter.incrementAndGet(); }
修复说明
computeIfAbsent是原子性方法,保证"检查key是否存在-不存在则创建并放入-返回值"整个流程不会被其他线程打断,彻底消除初始化阶段的竞态。incrementAndGet是AtomicInteger的原子自增方法,直接完成"取值-加1-赋值"的原子操作,避免计数更新时的竞态问题。
内容的提问来源于stack exchange,提问作者Pushpak
相关产品推荐
相关产品推荐

