ConcurrentHashMap、HashMap、Hashtable 三者区别?¶
结论速查表¶
| 维度 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ | ✅(方法级 synchronized) | ✅(分段锁 / CAS + synchronized) |
| 性能 | 最高 | 最差(全表锁) | 高(JDK 8 后) |
| null key / value | ✅ 允许 | ❌ 抛 NPE | ❌ 不允许 |
| 初始容量 | 16 | 11 | 16 |
| 扩容倍数 | 2x | 2x + 1 | 2x |
| 迭代器 | fail-fast | fail-fast + enumerator | 弱一致(weakly consistent) |
| 底层结构 | 数组+链表+红黑树(JDK8) | 数组+链表 | 数组+链表+红黑树 + CAS |
详细对比¶
1. HashMap¶
- 非线程安全,多线程下扩容可能成环形链表(JDK7 死循环)或数据丢失。
- 允许一个 null key、多个 null value。
- JDK8 后:数组 + 链表 + 红黑树(链表长度 ≥ 8 且数组长度 ≥ 64 树化)。
2. Hashtable¶
- 遗留类,几乎所有 public 方法都加
synchronized,锁的是整个 table 对象。 - 多线程下所有操作串行,性能极差。
- 不允许 null key/value,否则 NPE。
- 现在基本被
ConcurrentHashMap取代。
3. ConcurrentHashMap¶
- JDK7:分段锁
Segment[],每个 Segment 是一个小 Hashtable,并发度 = Segment 数(默认 16)。 - JDK8:废弃 Segment,改为
数组 + 链表 + 红黑树,用CAS + synchronized锁单个桶头节点,并发度 = 桶数。
// JDK8 put 片段
if (f == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // CAS 成功直接插入
}
synchronized (f) { // 锁桶头
// 链表/红黑树插入
}
怎么选¶
- 单线程或方法内局部变量:用
HashMap。 - 多线程共享 Map:用
ConcurrentHashMap,不要用 Hashtable。 - 需要排序:
TreeMap;需要并发排序:ConcurrentSkipListMap。
常见误区
Collections.synchronizedMap(new HashMap<>())只是把每个方法包一层 synchronized,性能比 ConcurrentHashMap 差。- ConcurrentHashMap 的迭代器是弱一致的,不会抛 ConcurrentModificationException,也不保证看到迭代过程中的修改。