Hashtable因全方法synchronized锁整个对象导致高并发性能极低,且不支持null、接口陈旧、无fail-fast,已被ConcurrentHashMap替代;迁移需注意null兼容性和toString同步差异。

为什么 Hashtable 是线程安全却没人用
因为它的线程安全是靠给每个方法加 synchronized 实现的,锁的是整个对象。哪怕你只查一个 key,其他所有读写操作都得排队——吞吐量极低,尤其在高并发场景下,Hashtable 几乎成了性能瓶颈本身。
常见错误现象:ConcurrentModificationException 很少出现在 Hashtable 里(因为全锁),但实际压测时你会发现 CPU 老是卡在 get() 和 put() 的同步块里,QPS 上不去。
-
Hashtable不允许nullkey 或nullvalue,一放就抛NullPointerException,而现代业务数据经常带null字段,兼容性差 - 它继承自古老的
Dictionary类(JDK 1.0 就有),接口设计僵硬,比如没有forEach()、computeIfAbsent()这类函数式方法 - 和
HashMap不同,Hashtable没有 fail-fast 迭代器机制,遍历时即使被其他线程修改也不会报错,容易掩盖并发 bug
ConcurrentHashMap 替代方案怎么选版本
不同 JDK 版本的 ConcurrentHashMap 实现差异很大,不能只看类名就认为“线程安全=能直接换”。JDK 7 用分段锁(Segment),JDK 8 改为 synchronized + CAS + 红黑树,JDK 9+ 进一步优化了扩容逻辑。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- JDK 8+ 项目,直接用
ConcurrentHashMap,别配initialCapacity过大(默认 16 就够,扩容成本比Hashtable低得多) - 如果必须支持 JDK 7,注意
size()返回的是估算值(各Segment计数之和,可能不准),而 JDK 8+ 的size()是精确的 - 避免调用
elements()或keys()(Hashtable的遗留方法),它们返回的是不支持快速失败的枚举,换成keySet().iterator()
HashMap + Collections.synchronizedMap() 为什么也不推荐
这看似是个折中方案:用 HashMap 的性能 + 外层同步包装。但它和 Hashtable 本质一样——所有方法都串行化,只是锁对象从 this 换成了包装器实例。
更关键的问题是:它**不保证复合操作的原子性**。比如下面这段代码:
Mapmap = Collections.synchronizedMap(new HashMap<>()); if (!map.containsKey("count")) { map.put("count", 1); // 这里可能被其他线程插队 }
上面的判断 + 插入不是原子的,两个线程同时进来就会覆盖。而 ConcurrentHashMap 提供 computeIfAbsent() 或 putIfAbsent() 直接解决这类问题。
遗留系统里遇到 Hashtable 怎么安全迁移
不能直接把 Hashtable 换成 ConcurrentHashMap,因为两者对 null 的处理不兼容——前者拒绝,后者允许。一旦旧代码里隐式依赖“不会出现 null”,替换后可能引发空指针。
稳妥做法:
- 先全局搜索
new Hashtable 和instanceof Hashtable,确认所有使用点 - 对明确不存
null的场景,用ConcurrentHashMap+ 构造时传入loadFactor=0.75f(和Hashtable默认一致) - 对不确定数据源的,加一层包装类,重写
put()和putAll(),拦截null并抛出明确异常,暴露问题而不是静默失败
最常被忽略的一点:Hashtable 的 toString() 是同步的,而 ConcurrentHashMap 不是——如果日志里直接打印 map,高并发下可能触发内部结构不一致导致的异常输出。需要加临时同步或改用 new TreeMap(map).toString() 做快照。










