HashMap 和 ConcurrentHashMap 的区别.md
- 放入 HashMap 的元素是 key-value 对。
- 底层说白了就是散列结构。
- 要将元素放入到 HashMap 中,那么 key 的类型必须要实现 hashcode 方法,默认这个方法是根据对象的地址来计算的,接着还必须覆盖对象的 equals() 方法。
- ConcurrentHashMap 对整个桶数组进行了分段,而 HashMap 则没有
- ConcurrentHashMap 在每一个分段上都用锁进行保护,从而让锁的粒度更精细一些,并发性能更好,而 HashMap 没有锁机制,不是线程安全的
五条要点方向正确,但”分段锁”是 JDK7 的描述,JDK8 已改为 CAS + synchronized 桶级锁;且两者差异不止同步机制,还有 null 语义、迭代器一致性、size 统计等。本文按”结构同源、并发异构”的主线补全。
同源:散列结构与键契约
两者底层都是”数组 + 链表/红黑树”(JDK8),都依赖 key 的 hashCode 定位桶、equals 判等。key 契约相同:重写 equals 必须重写 hashCode,否则检索错乱。 put/get 的单线程语义也一致(同键覆盖、O(1) 期望复杂度)。
异构一:同步机制的代际
| 版本 | HashMap | ConcurrentHashMap |
|---|---|---|
| 并发安全 | 否 | 是 |
| JDK7 实现 | 无锁 | Segment 分段锁(16 段 ReentrantLock) |
| JDK8 实现 | 无锁 | CAS 写空桶 + synchronized 锁桶头;扩容多线程协作迁移 |
| 读路径 | 无锁 | 无锁(volatile 保证可见性) |
JDK8 的关键进化:锁粒度从”段”细化到”桶”(冲突才锁),读完全无锁,扩容时多线程按 stride 认领区间协作搬迁(ForwardingNode 标记)。并发度不再受固定段数限制。
异构二:null 语义
- HashMap:允许 1 个 null 键、多个 null 值
- ConcurrentHashMap:null 键 null 值都禁止(put 时直接 NPE)
原因深刻:并发下 get(k)==null 无法区分”键不存在”与”值为 null”(单线程 HashMap 可用 containsKey 二次确认,并发下两次调用间状态可能已变)。为消除歧义,CHM 选择禁 null——这是并发容器 API 设计的经典取舍。
异构三:迭代器与 size 的一致性模型
- HashMap 迭代器 fail-fast:并发修改抛 ConcurrentModificationException
- CHM 迭代器弱一致性(weakly consistent):不抛 CME,反映创建时刻及之后的部分更新,允许遍历期间集合变化——并发遍历可用性的代价是”快照不精确”
- size():HashMap 直接返回维护的 size;CHM 用 baseCount + CounterCell 分片累加(LongAdder 思想),高并发下 size 是估计值语义(sumCount),文档明确不保证精确
异构四:原子复合操作
CHM 提供 putIfAbsent、compute、computeIfAbsent、merge 等单原子操作,把 check-then-act 收进容器内部:
cache.computeIfAbsent(key, k -> loadFromDb(k)); // 原子:同键只加载一次 |
HashMap 要实现同等语义只能外部加锁——这正是”并发容器不只是加锁的容器”的含义:它把复合操作的原子性做进了实现。注意 computeIfAbsent 的映射函数中不要再操作本 map(重入风险)。
选型清单
- 单线程 / 线程封闭(方法内局部变量)→ HashMap
- 多线程共享读写 → ConcurrentHashMap(唯一推荐)
- 读多写极少且整体替换 → 不可变 Map + volatile 引用,或 CopyOnWrite 语义结构
- 需要 null 键值的并发场景 → CHM 不支持,考虑哨兵值或外层包装
- 统计计数类 → CHM + merge/LongAdder,而非 synchronized(HashMap)
HashMap 与 ConcurrentHashMap 的区别,表层是”有无锁”,里层是一致性模型的整体设计:null 禁令、弱一致迭代器、估计 size、原子复合操作,都是”并发可用”这一目标下的配套取舍。把它们当作一个体系理解,比逐条背差异稳固得多。











