ConcurrentHashMap
与 HashMap
都是基于哈希表的数据结构,但它们的设计目标和并发行为完全不同。以下是从核心结构、线程安全性、性能、适用场景等方面的对比:
✅ 一、基本区别概览
特性 | HashMap | ConcurrentHashMap |
---|---|---|
是否线程安全 | ❌ 否 | ✅ 是(高并发下仍安全) |
并发策略 | 无 | JDK1.7:分段锁JDK1.8:CAS + synchronized |
null 键/值是否允许 | ✅ 允许一个 null 键和多个 null 值 | ❌ 都不允许 |
读写性能(并发场景) | 低,可能死循环、数据丢失 | 高,并发控制精细 |
扩容机制是否安全 | ❌ 否 | ✅ 是 |
JDK 版本支持 | JDK1.2+ | JDK1.5+ |
✅ 二、线程安全性
🔸 HashMap
-
所有方法都没有同步控制
-
多线程下可能:
-
数据覆盖、丢失
-
链表成环(JDK1.7),导致
get
死循环 -
非原子操作(如
putIfAbsent
)无法保证一致性
-
🔸 ConcurrentHashMap
-
JDK1.7:
-
使用 Segment 分段锁(每个 Segment 是一个小 HashMap + ReentrantLock)
-
并发控制粒度较粗,但较 HashMap 安全
-
-
JDK1.8(重点):
-
放弃 Segment,使用:
-
CAS
保证节点插入安全 -
synchronized
替代全锁,锁粒度控制在桶(bin)级别 -
Node<K,V>[]
+ 链表 + 红黑树结构
-
-
并发更高效,读操作多数是无锁的
-
✅ 三、数据结构差异
JDK 版本 | HashMap 底层结构 | ConcurrentHashMap 底层结构 |
---|---|---|
JDK1.7 | 数组 + 链表(头插法) | Segment[] + HashEntry 链表 |
JDK1.8+ | 数组 + 链表 + 红黑树(尾插) | Node[] + 链表 + 红黑树 + synchronized + CAS |
✅ 四、是否允许 null
类 | null 作为 key | null 作为 value |
---|---|---|
HashMap | ✅ 是 | ✅ 是 |
ConcurrentHashMap | ❌ 否 | ❌ 否 |
原因:ConcurrentHashMap 内部使用 null 判断 key 是否存在,加入 null 会破坏语义。
✅ 五、常用场景对比
场景 | 推荐使用 |
---|---|
单线程、非并发环境 | HashMap |
多线程读写(高并发) | ConcurrentHashMap |
Map 需要缓存结构、延迟加载等 | ConcurrentHashMap (支持原子操作) |
✅ 六、API 差异(部分示例)
操作 | HashMap | ConcurrentHashMap |
---|---|---|
putIfAbsent | ❌ 手动实现 | ✅ 原子操作 |
computeIfAbsent | ❌ 需加锁实现 | ✅ 支持并发懒加载 |
merge | ❌ | ✅ 支持 |
forEach 并发安全遍历 | ❌ | ✅ 避免 ConcurrentModificationException |
✅ 七、小结
对比点 | HashMap | ConcurrentHashMap |
---|---|---|
并发安全 | ❌ 不是 | ✅ 是(读无锁,写粒度细) |
并发性能 | 差,多线程容易出错 | 高,适用于高并发缓存/共享结构 |
null 支持 | 支持 null key 和 null value | 都不支持,设计上明确拒绝 |
使用建议 | 单线程或只读环境 | 多线程读写共享结构 |
如你有意深入了解 ConcurrentHashMap
在 JDK1.8 中的核心实现细节,如 bin 锁设计、红黑树转换、CAS 插入机制等,我可以继续展开。是否需要?