ConcurrentHashMap是Java并发包中最重要的容器之一,它解决了传统Hashtable全表锁性能低下的问题,同时保证了多线程环境下的数据一致性。在电商秒杀、本地缓存、统计计数等高并发场景中,几乎都能看到它的身影。想真正用好这个容器,需要理解它的底层实现、掌握常用的原子方法,并清楚它的使用边界。本文将从实现原理、常用技巧、常见陷阱三个层面展开讲解。

一、ConcurrentHashMap的底层实现原理
在JDK 7时代,ConcurrentHashMap采用分段锁(Segment)设计,将整个数据结构拆分成多个段,每个段独立加锁,不同段之间的操作可以并行执行。这种设计把锁粒度从整张表降到了一段,并发度默认为16。
到了JDK 8,实现被彻底重写。分段锁被废弃,取而代之的是Node数组加链表加红黑树的结构,锁粒度进一步缩小到单个桶的头节点。写线程在修改某个桶时,只需对该桶的头节点加synchronized锁,其他桶的读写完全不受影响。这种设计的锁粒度更细,理论上并发度等于数组长度。
读操作则完全不加锁。ConcurrentHashMap的内部Node结构和关键字段都使用volatile修饰,保证了一个线程的写入对其他线程立即可见。读取时通过Unsafe类的原子操作直接访问内存,配合volatile语义,实现了无锁化的高效读取。这也是它吞吐量远超Hashtable的根本原因。
初始化与扩容也体现了精巧的并发设计。数组初始化通过CAS操作修改sizeCtl字段控制,保证只有一个线程执行初始化。扩容时多个线程可以协作迁移数据,每个线程认领一段桶区间进行处理,迁移完成的桶会被标记为ForwardingNode,指引后续访问线程去新表中查找。
二、常用API与高并发使用技巧
最基础的使用方式和HashMap类似,调用put、get、remove方法即可。但在并发场景下,更推荐使用ConcurrentHashMap提供的原子复合方法,因为它们内部保证了检查和写入的原子性,不需要外部加锁。
比如实现一个线程安全的计数统计,如果用传统写法,先get判断再put,两个线程可能同时判断到key不存在,导致数据覆盖:
Map<String, Integer> counter = new ConcurrentHashMap<>();
// 错误示范:非原子的复合操作
Integer old = counter.get("pv");
if (old == null) {
counter.put("pv", 1);
} else {
counter.put("pv", old + 1);
}正确做法是使用merge或compute方法,它们在内部对桶头节点加锁执行,保证原子性:
Map<String, Integer> counter = new ConcurrentHashMap<>();
// 原子累加,key不存在时初始值为0,否则加1
counter.merge("pv", 1, Integer::sum);
// 等价写法
counter.compute("pv", (k, v) -> v == null ? 1 : v + 1);computeIfAbsent是构建本地缓存最常用的方法,key不存在时才计算并放入,天然实现了惰性加载,且同一key的初始化只执行一次:
Map<String, List<String>> cache = new ConcurrentHashMap<>();
List<String> list = cache.computeIfAbsent("orderList", k -> loadFromDb(k));
// 注意:mappingFunction返回null时不会写入任何值另外还有几个实用技巧值得掌握。putIfAbsent适合实现分布式锁的本地版本或注册表;size和isEmpty这类方法在并发环境下只是估算值,统计类的精确需求建议用LongAdder配合value使用;如果需要遍历时并发修改,ConcurrentHashMap的迭代器是弱一致性的,不会抛ConcurrentModificationException,但可能读不到最新写入的数据。
三、常见陷阱与避坑指南
第一个坑是key和value都不允许为null。与HashMap不同,ConcurrentHashMap的get返回null时无法区分是key不存在还是value本身就是null,出于二义性考虑,设计者直接禁止了null值。如果业务上确实需要表达空值,可以用Optional或自定义占位对象替代。
第二个坑是复合操作的原子性误区。单个方法是线程安全的,但多个方法组合起来就不安全了。典型如先containsKey再put的写法,在并发下会出现竞态条件。所有这类场景都应该用putIfAbsent、compute、merge等原子方法替代。
第三个坑是在compute和computeIfAbsent的回调函数中修改当前Map。JDK 8中这样做可能直接导致死循环或异常,因为回调执行时已经持有桶头节点的锁,回调内再次触发对同一桶的写操作会自我阻塞。正确做法是把计算逻辑保持纯粹,副作用放到回调外部执行。
第四个坑是把ConcurrentHashMap当作万能的性能方案。它只保证容器内部操作的线程安全,业务层面的原子性仍需自己保证。例如先读再判断再写的业务流程,即使容器本身线程安全,整体逻辑依然可能出错,必要时还是要考虑显式锁或改用数据库层面的原子更新。
四、性能对比与选型建议
从实测数据看,在16个线程并发写的场景下,ConcurrentHashMap的吞吐量通常是Hashtable的十倍以上,读多写少场景优势更明显。Collections.synchronizedMap虽然写法兼容HashMap,但内部仍是方法级同步,性能与Hashtable接近,仅适合并发度极低的过渡场景。
选型时可以参考以下原则:纯单线程环境用HashMap即可,性能最优;低并发且需要简单同步时可用Collections.synchronizedMap;大多数并发场景首选ConcurrentHashMap;需要频繁统计总数时,考虑ConcurrentSkipListMap排序需求或直接用LongAdder承担计数职责。对于key有序需求的并发场景,ConcurrentSkipListMap是唯一选择,它是基于跳表实现的有序并发Map。
最后提醒一点,构造ConcurrentHashMap时如果能预估容量,尽量通过构造函数指定initialCapacity,避免频繁扩容带来的开销。默认初始容量是16,负载因子0.75,在写密集场景下合理设置容量参数对性能提升明显。
ConcurrentHashMapJava并发高并发Map修改时间:2026-09-09 05:26:32