ComputeIfAbsent引发Map值重复问题(Short类型场景)
并发场景下ConcurrentHashMap.computeIfAbsent生成重复Short ID的原因及解决方案
问题根源
你遇到的重复ID问题,核心并非computeIfAbsent本身线程不安全,而是valueFactory中的idByName.size()调用不具备原子性,无法保证并发下的唯一性:
ConcurrentHashMap.size()的返回值在并发修改时是近似值:它通过遍历所有分段的计数器累加得到,多线程同时添加元素时,不同线程可能读到同一个旧的size值。- 针对不同key的
computeIfAbsent调用可以并发执行:当两个线程同时往同一个子ConcurrentHashMap中添加不同key时,它们的valueFactory会同时读取当前size。如果此时子map还未完成任何新元素的插入,两个线程都会拿到相同的size(比如0),转成Short后就会生成重复ID。
关于“改用Integer类型解决问题”的误解
你提到改用Integer类型可解决问题,这大概率是测试时的线程调度巧合——Integer类型本身并不能解决并发计数的原子性问题,只是某次测试中刚好没触发并发读取size的场景,本质上依然存在重复ID的风险。
正确解决方案
不要依赖ConcurrentHashMap.size()生成唯一ID,改用AtomicInteger作为每个子map的独立计数器,确保计数操作是原子性的。修改后的代码如下:
Java 8+ 版本(使用Pair)
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.tuple.Pair; public class UniqueIdGenerator { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(4); final Map<String, Pair<AtomicInteger, ConcurrentHashMap<String, Short>>> threadSafeMap = new ConcurrentHashMap<>(); threadSafeMap.put("0", Pair.of(new AtomicInteger(0), new ConcurrentHashMap<>())); threadSafeMap.put("1", Pair.of(new AtomicInteger(0), new ConcurrentHashMap<>())); for (int i = 1; i <= 10; i++) { final int jj = i; executorService.submit(() -> { int key = jj % 2; Pair<AtomicInteger, ConcurrentHashMap<String, Short>> pair = threadSafeMap.get(String.valueOf(key)); AtomicInteger counter = pair.getKey(); ConcurrentHashMap<String, Short> idByName = pair.getValue(); return idByName.computeIfAbsent(String.valueOf(jj), x -> (short) counter.getAndIncrement()); }); } executorService.shutdown(); executorService.awaitTermination(5, TimeUnit.SECONDS); // 输出结果 threadSafeMap.forEach((k, v) -> System.out.println(k + "=" + v.getValue())); } }
Java 16+ 版本(使用Record更简洁)
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class UniqueIdGenerator { // 用Record封装计数器和映射表 private record CounterMap(AtomicInteger counter, ConcurrentHashMap<String, Short> map) {} public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(4); final Map<String, CounterMap> threadSafeMap = new ConcurrentHashMap<>(); threadSafeMap.put("0", new CounterMap(new AtomicInteger(0), new ConcurrentHashMap<>())); threadSafeMap.put("1", new CounterMap(new AtomicInteger(0), new ConcurrentHashMap<>())); for (int i = 1; i <= 10; i++) { final int jj = i; executorService.submit(() -> { int key = jj % 2; CounterMap counterMap = threadSafeMap.get(String.valueOf(key)); return counterMap.map().computeIfAbsent(String.valueOf(jj), x -> (short) counterMap.counter().getAndIncrement()); }); } executorService.shutdown(); executorService.awaitTermination(5, TimeUnit.SECONDS); // 输出结果 threadSafeMap.forEach((k, v) -> System.out.println(k + "=" + v.map())); } }
方案说明
AtomicInteger.getAndIncrement()是原子操作,能保证每个线程都拿到唯一的递增数值,彻底避免并发下的重复计数问题。- 每个子map对应独立的原子计数器,分组内的ID生成完全隔离,符合你的业务逻辑。
内容的提问来源于stack exchange,提问作者Ahmad Darwich
相关产品推荐
相关产品推荐

