• 放入 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));  // 原子:同键只加载一次
counter.merge(k, 1L, Long::sum); // 原子计数

HashMap 要实现同等语义只能外部加锁——这正是”并发容器不只是加锁的容器”的含义:它把复合操作的原子性做进了实现。注意 computeIfAbsent 的映射函数中不要再操作本 map(重入风险)。

选型清单

  • 单线程 / 线程封闭(方法内局部变量)→ HashMap
  • 多线程共享读写 → ConcurrentHashMap(唯一推荐)
  • 读多写极少且整体替换 → 不可变 Map + volatile 引用,或 CopyOnWrite 语义结构
  • 需要 null 键值的并发场景 → CHM 不支持,考虑哨兵值或外层包装
  • 统计计数类 → CHM + merge/LongAdder,而非 synchronized(HashMap)

HashMap 与 ConcurrentHashMap 的区别,表层是”有无锁”,里层是一致性模型的整体设计:null 禁令、弱一致迭代器、估计 size、原子复合操作,都是”并发可用”这一目标下的配套取舍。把它们当作一个体系理解,比逐条背差异稳固得多。