1. 为什么我们今天还要聊Hashtable?
如果你是一个Java开发者,可能已经很久没有在代码里写过 new Hashtable<>() 了。确实,在大多数现代Java应用中,HashMap 和 ConcurrentHashMap 几乎已经接管了所有键值对存储的场景。那为什么我们还要花时间,去深入一个看似“过时”的类呢?这就像问一个赛车手为什么还要了解老式手动变速箱的原理一样——知其然,更要知其所以然。
Hashtable是Java集合框架中线程安全哈希表的“活化石”。它身上那种简单粗暴的线程安全实现方式(给整个方法加 synchronized),以及它那独特的扩容策略(容量翻倍再加一),都是理解Java并发容器演进史的绝佳样本。我在处理一些遗留系统时,就曾多次遇到过因为对Hashtable性能特性理解不足而导致的线上瓶颈。通过剖析它的源码,你不仅能彻底搞懂一个经典数据结构的运作机制,更能深刻理解为什么后来的 ConcurrentHashMap 要采用完全不同的设计思路。这对于你在高并发场景下做出正确的技术选型,以及进行深层次的性能调优,有着不可替代的价值。
所以,这篇文章不是一份简单的API说明书。我会带你直接钻进JDK源码,看看Hashtable的 synchronized 到底锁住了什么,它的扩容(rehash)过程在并发下会引发什么问题,并和 ConcurrentHashMap 做一次面对面的对比。目标是让你下次在代码评审或架构设计时,谈到线程安全的Map,心里能有清晰的图谱和底气。
2. 线程安全的基石:synchronized方法与全局锁
Hashtable的线程安全,可以说是Java早期并发设计的一个典型代表:简单、直接,但代价不小。它的核心秘密就藏在几乎每一个公共方法的签名里。
2.1 锁住整个“世界”的synchronized
打开JDK中Hashtable的源码,你会发现 put, get, remove, size, containsKey 等所有会修改或读取数据的方法,都加上了 synchronized 关键字。这意味着什么?这意味着这些方法都是实例同步方法,锁住的是整个Hashtable对象本身(即 this)。
// JDK 1.8 Hashtable.java 源码片段
public synchronized V put(K key, V value) {
// 确保值不为null
if (value == null) {
throw new NullPointerException();
}
// ... 后续插入逻辑
}
public synchronized V get(Object key) {
// ... 查找逻辑
}
public synchronized V remove(Object key) {
// ... 删除逻辑
}
我画个简单的场景你就明白了。假设有两个线程,线程A正在执行 hashtable.put("key1", "value1"),此时线程B试图调用 hashtable.get("key2")。会发生什么?线程B会被阻塞,它必须等待线程A完全执行完 put 方法并释放锁之后,才能进入 get 方法。即使它们操作的是完全不同的键,访问的是哈希表中不同的链表(桶),也照样要排队。
这种锁的粒度太粗了。它相当于把整个哈希表当作一个临界资源,任何时候只允许一个线程进行任何操作。在高并发读多写少的场景下,这种设计会带来严重的性能瓶颈。我实测过一个简单的压测案例:用10个线程并发对一个Hashtable进行100万次读操作,其吞吐量可能只有使用 ConcurrentHashMap 的十分之一甚至更低。
2.2 迭代器的“快速失败”与并发修改的尴尬
Hashtable的线程安全仅限于它的单个方法调用。当你需要遍历它时,问题就来了。通过 iterator() 或 keys()、elements() 等方法获得的迭代器,是“快速失败”(fail-fast)的。
Hashtable<String, Integer> table = new Hashtable<>();
table.put("a", 1);
table.put("b", 2);
Iterator<String> it = table.keySet().iterator();
while (it.hasNext()) {
String

2万+

被折叠的 条评论
为什么被折叠?



