核心概念
这三个都是 Java 中常用的 Map 实现,主要区别在于线程安全性和性能特征:
- HashMap:线程不安全,性能最高,适用于单线程或外部同步场景
- Hashtable:线程安全(方法级同步),性能较低,已基本被淘汰
- ConcurrentHashMap:线程安全(分段锁/CAS),高并发性能优异,是并发场景首选
核心区别对比
1. 线程安全性
Hashtable:
- 所有方法使用
synchronized修饰 - 粒度粗,整个表锁定,并发度为 1
- 读写操作都会阻塞其他线程
public synchronized V get(Object key) { ... }
public synchronized V put(K key, V value) { ... }
HashMap:
- 完全不同步,多线程下可能导致数据不一致甚至死循环(JDK 1.7)
ConcurrentHashMap:
- JDK 1.7:分段锁(Segment 数组),默认 16 个段,并发度 16
- JDK 1.8+:CAS + synchronized(锁桶头节点),并发度等于桶数量
- 读操作几乎无锁(
volatile保证可见性),写操作只锁单个桶
// JDK 1.8 put 操作核心逻辑
final V putVal(K key, V value, boolean onlyIfAbsent) {
Node<K,V>[] tab; Node<K,V> f; int n, i, fh;
// 1. 如果桶为空,CAS 设置
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 成功则退出
}
// 2. 如果有冲突,synchronized 锁住桶头节点
else {
synchronized (f) {
// 链表或红黑树插入逻辑
}
}
}
2. Null 键值支持
| 实现类 | null key | null value |
|---|---|---|
| HashMap | 允许 1 个 | 允许多个 |
| Hashtable | 不允许 | 不允许 |
| ConcurrentHashMap | 不允许 | 不允许 |
原因:
- HashMap 单线程场景,
get(key)返回null可明确区分”键不存在”和”值为 null”(通过containsKey) - 并发容器中,
get(key)返回null无法区分这两种情况(存在竞态条件),故禁止 null
3. 底层结构
Hashtable:
- 数组 + 链表(无红黑树优化)
- 初始容量 11,扩容为
2n + 1
HashMap:
- 数组 + 链表 + 红黑树(JDK 1.8+)
- 链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树
- 初始容量 16,扩容为
2n(保证容量为 2 的幂)
ConcurrentHashMap:
- JDK 1.7:Segment 数组 + HashEntry 数组
- JDK 1.8:与 HashMap 相似(数组 + 链表 + 红黑树),但节点使用
volatile
4. 性能对比
| 场景 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 单线程读写 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 多线程读 | ❌ 不安全 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 多线程写 | ❌ 不安全 | ⭐ | ⭐⭐⭐⭐⭐ |
| 高并发场景 | ❌ | ⭐ | ⭐⭐⭐⭐⭐ |
使用场景与选型
HashMap
// 单线程或外部同步
Map<String, User> userMap = new HashMap<>();
// 需要线程安全可包装(性能差,不推荐)
Map<String, User> syncMap = Collections.synchronizedMap(new HashMap<>());
ConcurrentHashMap
// 高并发场景首选
Map<String, User> cache = new ConcurrentHashMap<>();
// 原子操作
cache.putIfAbsent("key", new User());
cache.computeIfAbsent("key", k -> new User());
Hashtable
// ❌ 已过时,不推荐使用
// 如需线程安全,优先选择 ConcurrentHashMap
并发场景的注意事项
1. HashMap 在多线程下的问题
JDK 1.7 死循环问题:
- 扩容时采用头插法,多线程可能形成环形链表
get()操作陷入死循环,CPU 100%
JDK 1.8 改进:
- 改为尾插法,但仍可能丢失数据或覆盖
2. ConcurrentHashMap 的弱一致性
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.put("b", 2);
// 迭代过程中的修改可能可见也可能不可见
for (String key : map.keySet()) {
map.put("c", 3); // 可能在迭代中体现,也可能不体现
}
3. 复合操作需额外同步
// ❌ 非原子操作
if (!map.containsKey(key)) {
map.put(key, value); // 存在竞态条件
}
// ✅ 使用原子方法
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> computeValue(k));
面试总结
| 维度 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ | ✅(方法锁) | ✅(分段锁/CAS) |
| null 支持 | ✅ | ❌ | ❌ |
| 性能 | 最高 | 低 | 高 |
| 推荐场景 | 单线程 | 不推荐 | 并发场景 |
| JDK 版本 | 1.2+ | 1.0(遗留类) | 1.5+(1.8 重写) |
记忆口诀:
- HashMap:快但不安全
- Hashtable:安全但慢(已淘汰)
- ConcurrentHashMap:又快又安全(并发首选)