Java中Hashtable和ConcurrentHashMap的區別
1. 概述
在 Java 應用程序中管理鍵值對時,我們經常發現自己考慮兩個主要選項: Hashtable
和ConcurrentHashMap
。
雖然這兩個集合都提供了線程安全的優勢,但它們的底層架構和功能卻存在顯著差異。無論我們是構建遺留系統還是開發基於微服務的現代云應用程序,了解這些細微差別對於做出正確的選擇至關重要。
在本教程中,我們將剖析Hashtable
和ConcurrentHashMap
之間的差異,深入研究它們的性能指標、同步功能和其他各個方面,以幫助我們做出明智的決定。
2. Hashtable
Hashtable
是 Java 中最古老的集合類之一,自 JDK 1.0 以來就已存在。它提供鍵值存儲和檢索 API:
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("Key1", "1");
hashtable.put("Key2", "2");
hashtable.putIfAbsent("Key3", "3");
String value = hashtable.get("Key2");
Hashtable
的主要賣點是線程安全,這是通過方法級同步來實現的。
put(), putIfAbsent(), get(), and remove()
方法是同步的。在Hashtable
實例上,在給定時間只有一個線程可以執行這些方法中的任何一個,從而確保數據一致性。
ConcurrentHashMap
ConcurrentHashMap
是一種更現代的替代方案,作為 Java 5 的一部分隨 Java Collections Framework 一起引入。
Hashtable
和ConcurrentHashMap
都實現了Map
接口,這說明了方法簽名的相似性:
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", "1");
concurrentHashMap.put("Key2", "2");
concurrentHashMap.putIfAbsent("Key3", "3");
String value = concurrentHashMap.get("Key2");
4. 差異
在本節中,我們將研究區分Hashtable
和ConcurrentHashMap
的關鍵方面,包括並發性、性能和內存使用情況。
4.1.並發性
正如我們前面討論的, Hashtable
通過方法級同步來實現線程安全。
另一方面, ConcurrentHashMap
提供了線程安全性和更高級別的並發性。它允許多個線程同時讀取和執行有限的寫入,而無需鎖定整個數據結構。這對於讀操作多於寫操作的應用程序特別有用。
4.2 性能
雖然Hashtable
和ConcurrentHashMap
都保證了線程安全,但由於底層的同步機制,它們在性能上有所不同。
Hashtable
在寫入操作期間鎖定整個表,從而防止其他讀取或寫入。這可能是高並發環境中的瓶頸。
然而, ConcurrentHashMap
允許並發讀取和有限的並發寫入,使其更具可擴展性並且在實踐中通常更快。
對於小型數據集,性能數字的差異可能並不明顯。然而, ConcurrentHashMap
通常在更大的數據集和更高的並發級別上顯示出其優勢。
為了證實性能數據,讓我們使用 JMH(Java Microbenchmark Harness)運行基準測試,它使用 10 個線程來模擬並發活動,並執行 3 次預熱迭代,然後執行 5 次測量迭代。它測量每個基準方法所花費的平均時間,表示平均執行時間:
@Benchmark
@Group("hashtable")
public void benchmarkHashtablePut() {
for (int i = 0; i < 10000; i++) {
hashTable.put(String.valueOf(i), i);
}
}
@Benchmark
@Group("hashtable")
public void benchmarkHashtableGet(Blackhole blackhole) {
for (int i = 0; i < 10000; i++) {
Integer value = hashTable.get(String.valueOf(i));
blackhole.consume(value);
}
}
@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapPut() {
for (int i = 0; i < 10000; i++) {
concurrentHashMap.put(String.valueOf(i), i);
}
}
@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapGet(Blackhole blackhole) {
for (int i = 0; i < 10000; i++) {
Integer value = concurrentHashMap.get(String.valueOf(i));
blackhole.consume(value);
}
}
以下是測試結果:
Benchmark Mode Cnt Score Error
BenchMarkRunner.concurrentHashMap avgt 5 1.788 ± 0.406
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapGet avgt 5 1.157 ± 0.185
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapPut avgt 5 2.419 ± 0.629
BenchMarkRunner.hashtable avgt 5 10.744 ± 0.873
BenchMarkRunner.hashtable:benchmarkHashtableGet avgt 5 10.810 ± 1.208
BenchMarkRunner.hashtable:benchmarkHashtablePut avgt 5 10.677 ± 0.541
基準測試結果提供了對Hashtable
和ConcurrentHashMap
特定方法的平均執行時間的深入了解。
分數越低表示性能越好,結果表明,平均而言, ConcurrentHashMap
在get()
和put()
操作方面都優於Hashtable
。
4.3. Hashtable
迭代器
Hashtable
迭代器是“快速失敗”的,這意味著如果在創建迭代器後修改Hashtable
的結構,則迭代器將拋出ConcurrentModificationException
。當檢測到並發修改時,此機制會快速失敗,從而有助於防止不可預測的行為。
在下面的示例中,我們有一個包含三個鍵值對的Hashtable
,並且我們啟動兩個線程:
-
iteratorThread
:迭代Hashtable
鍵並以100
毫秒延遲打印它們 -
modifierThread
:等待50
毫秒,然後將新的鍵值對添加到Hashtable
中
當modifierThread
向Hashtable
添加一個新的鍵值對時, iteratorThread
拋出ConcurrentModificationException
,表明在迭代過程中Hashtable
結構被修改:
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("Key1", 1);
hashtable.put("Key2", 2);
hashtable.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);
Thread iteratorThread = new Thread(() -> {
Iterator<String> it = hashtable.keySet().iterator();
try {
while (it.hasNext()) {
it.next();
Thread.sleep(100);
}
} catch (ConcurrentModificationException e) {
exceptionCaught.set(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread modifierThread = new Thread(() -> {
try {
Thread.sleep(50);
hashtable.put("Key4", 4);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
iteratorThread.start();
modifierThread.start();
iteratorThread.join();
modifierThread.join();
assertTrue(exceptionCaught.get());
4.4. ConcurrentHashMap
迭代器
與使用“快速失敗”迭代器的Hashtable
相比, ConcurrentHashMap
使用“弱一致”迭代器。
這些迭代器可以承受對原始映射的並發修改,反映創建迭代器時映射的狀態。它們也可能反映進一步的變化,但不能保證這樣做。因此,我們可以在一個線程中修改ConcurrentHashMap
,同時在另一個線程中迭代它,而不會出現ConcurrentModificationException
。
下面的示例演示了ConcurrentHashMap
中迭代器的弱一致性質:
-
iteratorThread
:迭代ConcurrentHashMap
鍵並以100
毫秒延遲打印它們 -
modifierThread
:等待50
毫秒,然後將新的鍵值對添加到ConcurrentHashMap
中
與Hashtable
“快速失敗”迭代器不同,這裡的弱一致迭代器不會拋出ConcurrentModificationException
。 iteratorThread
中的迭代器繼續執行,沒有任何問題,展示了ConcurrentHashMap
是如何針對高並發場景設計的:
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", 1);
concurrentHashMap.put("Key2", 2);
concurrentHashMap.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);
Thread iteratorThread = new Thread(() -> {
Iterator<String> it = concurrentHashMap.keySet().iterator();
try {
while (it.hasNext()) {
it.next();
Thread.sleep(100);
}
} catch (ConcurrentModificationException e) {
exceptionCaught.set(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread modifierThread = new Thread(() -> {
try {
Thread.sleep(50);
concurrentHashMap.put("Key4", 4);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
iteratorThread.start();
modifierThread.start();
iteratorThread.join();
modifierThread.join();
assertFalse(exceptionCaught.get());
4.5.記憶
Hashtable
使用簡單的數據結構,本質上是一個鍊錶數組。該數組中的每個桶存儲一個鍵值對,因此只有數組本身和鍊錶節點的開銷。沒有額外的內部數據結構來管理並發級別、負載因子或其他高級功能。因此, Hashtable
總體上消耗的內存較少。
ConcurrentHashMap
更複雜,由段數組組成,本質上是一個單獨的Hashtable
。這允許它同時執行某些操作,但也會為這些段對象消耗額外的內存。
對於每個段,它維護額外的信息,例如計數、閾值、負載因子等,這會增加其內存佔用。它動態調整段的數量及其大小以容納更多條目並減少衝突,這意味著它必須保留額外的元數據來管理這些,從而導致進一步的內存消耗。
5. 結論
在本文中,我們了解了Hashtable
和ConcurrentHashMap.
Hashtable
和ConcurrentHashMap
的目的都是以線程安全的方式存儲鍵值對。然而,我們看到ConcurrentHashMap
由於其先進的同步特性,通常在性能和可擴展性方面佔據上風。
Hashtable
仍然有用,並且在遺留系統或明確需要方法級同步的場景中可能更可取。了解我們應用程序的具體需求可以幫助我們在這兩者之間做出更明智的決定。
與往常一樣,示例的源代碼可以在 GitHub 上獲取。