Hashtable 是 JDK 1.0 時代引入的經典哈希表,繼承 Dictionary、實現 Map、Cloneable、Serializable。它採用拉鏈法解決衝突,所有公開方法均聲明 synchronized,實現線程安全。但其鎖粒度粗(整表鎖)、不支持 null 鍵值、擴容策略固定(newCapacity = old*2+1),在高併發與現代開發中已逐漸被 ConcurrentHashMap 取代。理解其內部機制有助於維護舊代碼與掌握同步集合演進脈絡。
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
private transient Entry<K,V>[] table; // 條目數組
private transient int count; // 實際元素數
private int threshold; // 擴容閾值
private float loadFactor; // 加載因子,默認 0.75f
private transient int modCount = 0; // 修改次數,用於 fail-fast
private static class Entry<K,V> implements Map.Entry<K,V> {
int hash; K key; V value; Entry<K,V> next;
// 構造器、clone、getKey、getValue、setValue(拒絕 null)、equals、hashCode...
}
}
所有公共方法(get、put、remove、size、containsKey…)均加 synchronized,等同於「全表鎖」。
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % table.length; // 正數化後取模
遍歷鏈表匹配 hash 且 key.equals。
public synchronized V put(K key, V value) {
if (value == null) throw new NullPointerException(); // 拒絕 null 值
// 1. 鍵存在 → 覆蓋值
// 2. 鍵不存在 → 檢查擴容 → 頭插法插入新 Entry → count++
}
protected void rehash() {
int oldCapacity = table.length;
int newCapacity = oldCapacity * 2 + 1; // 固定策略,結果為奇數
Entry[] newMap = new Entry[newCapacity];
// 重新哈希所有條目到新數組
table = newMap;
threshold = (int)(newCapacity * loadFactor);
}
擴容時機:count >= threshold。注意 rehash 非 final,子類可覆寫。
標準單向鏈表刪除,維護 prev 指針,更新 modCount、count,斷開引用助 GC。
Hashtable 同時支持舊式 Enumeration 與新式 Iterator,核心實現類 Enumerator 實現兩個接口:
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
// 從 table 末尾向前掃描,鏈表內向後遍歷
public boolean hasMoreElements() { ... }
public T nextElement() { ... }
public boolean hasNext() { return hasMoreElements(); }
public T next() {
if (modCount != expectedModCount) throw new ConcurrentModificationException();
return nextElement();
}
public void remove() { // 僅 Iterator 模式支持
if (!iterator) throw new UnsupportedOperationException();
// 同步刪除 lastReturned 條目
}
}
遍歷順序:數組索引遞減 + 鏈表順序,與插入順序無關。
keySet()、entrySet()、values() 返回的集合均通過 Collections.synchronizedSet/Collection 包裝,保證視圖操作同步:
public Set<K> keySet() {
if (keySet == null) keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
內部類 KeySet、EntrySet、ValueCollection 分別繼承 AbstractSet/AbstractCollection,委託給外部 Hashtable 方法實現。
writeObject:寫入容量、大小、所有鍵值對(數組逆序+鏈表順序)readObject:按讀取順序重建,容量計算公式 (elements * loadFactor) + elements/20 + 3,確保為奇數clone():深拷貝 table 數組及每條鏈表(遞歸 Entry.clone())| 方式 | 適用場景 | 線程安全 | 性能 |
|---|---|---|---|
entrySet().iterator() | 需鍵值對,現代代碼首選 | 同步視圖 + fail-fast | 高 |
keySet().iterator() | 僅需鍵 | 同步視圖 + fail-fast | 中(額外 get) |
values().iterator() | 僅需值 | 同步視圖 + fail-fast | 高 |
keys() / elements() | 舊代碼維護 | Enumeration 無 fail-fast | 中 |
為什麼遷移?
null 鍵值,業務受限ConcurrentHashMap 採用 CAS + synchronized 節點鎖 + 紅黑樹轉換,性能碾壓遷移對照表
| Hashtable | ConcurrentHashMap | 備註 |
|---|---|---|
synchronized put/get | 無鎖 get、分段/節點鎖 put | 並發度提升數量級 |
| 拒絕 null | 允許 null 值(鍵仍不可 null) | 業務更靈活 |
Enumeration | 弱一致性 Iterator | 遍歷不拋 CME |
Collections.synchronizedMap | 原生並發 | 無需額外包裝 |
代碼對比
// 舊代碼
Hashtable<String, Integer> table = new Hashtable<>();
table.put("a", 1);
// 新代碼
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
// 原子複合操作
map.computeIfAbsent("key", k -> expensiveCompute());
map.merge("key", 1, Integer::sum);
Hashtable 以「全方法同步」實現線程安全,結構簡單但擴展性差。其 Enumerator 雙模式遍歷、rehash 奇數擴容、集合視圖同步包裝等設計細節,反映了早期 JDK 的工程權衡。現代項目強烈建議遷移至 ConcurrentHashMap,獲得更高併發性能、更豐富原子操作 API 與弱一致性迭代器。僅在維護不可升級的舊系統時繼續使用 Hashtable,並注意其不支持 null、迭代器 fail-fast、複合操作需外部加鎖等限制。