在 Java 集合框架中,Hashtable 和 ConcurrentHashMap 都是线程安全的 Map 实现,常被用来在多线程环境下存储键值对。很多初学者容易把它们混为一谈,认为功能一样就可以互换使用,但实际上两者在设计理念、性能、功能和适用场景上存在巨大差异。
主要区别
| 对比项 | Hashtable | ConcurrentHashMap (JDK 8) | 说明与影响 |
|---|---|---|---|
| 出现版本 | JDK 1.0(非常古老) | JDK 1.5 引入,JDK 8 大幅重构 | Hashtable 是遗留类,ConcurrentHashMap 是现代高并发首选 |
| 线程安全实现方式 | 所有公共方法都加 synchronized(对象级锁) |
分段锁(JDK 7)→ CAS + synchronized(JDK 8) | Hashtable 锁粒度大,并发度低;ConcurrentHashMap 锁粒度细,并发性能高 |
| 锁粒度 | 整个对象一把锁 | JDK 8:单个桶(bucket)级别 synchronized + CAS | ConcurrentHashMap 支持更高的并发读写 |
| 读操作是否加锁 | 是(get 也加 synchronized) | 否(get 不加锁,基于 volatile 可见性) | ConcurrentHashMap 的 get 操作无锁,读取性能极高 |
| 允许 null 键/值 | 不允许(put null 会抛 NullPointerException) | 不允许键为 null,允许值为 null | 两者都不允许 null 键,但 Hashtable 连值也不允许 |
| 迭代器行为 | fail-fast(并发修改抛 ConcurrentModificationException) | weak consistent(弱一致性),迭代器不会抛异常 | ConcurrentHashMap 的迭代器能反映并发修改后的状态,更适合高并发遍历 |
| 容量扩容机制 | 容量为 2 倍 + 1,扩容时重新 hash | 容量为 2 的幂次方,JDK 8 扩容时高效 rehash | ConcurrentHashMap 扩容更高效,支持并发扩容 |
| size()、isEmpty() 准确性 | 准确(加锁计算) | 近似值(不加锁统计,可能有延迟) | 高并发下 ConcurrentHashMap 的 size() 不保证实时精确,但性能更好 |
| 性能(高并发场景) | 较低(全表锁) | 极高(细粒度锁 + 无锁读) | 实际生产环境中,ConcurrentHashMap 性能通常是 Hashtable 的数倍到数十倍 |
| 是否推荐继续使用 | 不推荐(已过时) | 强烈推荐 | 官方建议用 ConcurrentHashMap 替代 Hashtable |
两者都不支持 null 键,如果必须用 null,考虑 Collections.synchronizedMap(new HashMap<>())(但性能差)
深入细节说明
- 锁机制差异(最核心区别)
- Hashtable:几乎所有公共方法(put、get、remove、size 等)头部都直接加了 synchronized,相当于对整个 Hashtable 对象加锁。任意时刻只能有一个线程操作,严重限制并发能力。
- ConcurrentHashMap(JDK 8):
- get 操作完全无锁,依赖 Node 字段的 volatile 语义保证可见性。
- put/remove 操作只锁住对应的桶(bucket),其他桶可以并发操作。
- 使用 CAS(Compare-And-Swap)无锁操作 + 必要时 synchronized 锁单个桶。
- 扩容时支持多线程协同转移数据(transfer),进一步提升性能。
- 迭代器特性
- Hashtable 的迭代器是 fail-fast 类型的,一经发现并发修改就立刻抛异常。
- ConcurrentHashMap 的迭代器是弱一致性(weakly consistent),遍历过程中如果有其他线程修改,迭代器会尽量反映最新的数据,不会抛异常。这在高并发日志、监控统计等场景非常有用。
- null 值处理
- Hashtable:键和值都不能为 null,put(null) 会直接抛 NullPointerException。
- ConcurrentHashMap:键不能为 null(会抛 NullPointerException),但值可以为 null。
- 历史演进
- JDK 7 的 ConcurrentHashMap 使用 Segment 分段锁(默认 16 段)。
- JDK 8 彻底重构,放弃 Segment,改为更细粒度的桶级锁 + 红黑树优化(链表 >8 转树),性能和内存利用率都大幅提升。
代码对比示例
java
// Hashtable 示例(所有操作都加锁)
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("key", 1);
Integer value = hashtable.get("key");
// ConcurrentHashMap 示例(get 无锁,put 只锁对应槽)
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key", 1);
Integer value = concurrentMap.get("key"); // 高并发下极快
总结
Hashtable 是 Java 早期粗粒度同步的产物,已被时代淘汰;ConcurrentHashMap 是现代高并发设计的典范,拥有更高的吞吐量、更细的锁粒度、更友好的迭代器和更好的扩容机制。
生产环境中的最佳实践 : 永远优先使用 ConcurrentHashMap,彻底抛弃 Hashtable。