分析 java.util.Hashtable 原始碼

2022-07-15 18:01:34

概述

基於J11,該類已經淘汰,如果使用執行緒安全的則用 ConcurrentHashMap ,用執行緒不安全的則使用 HashMap 。僅與HashMap進行比較

結構以及依賴關係

HashTable 的結構如下圖

當遇到有同樣 Hash 值的情況,會通過連結串列來解決衝突問題(連結法,通過連結串列解決衝突問題)。
連結法會隨著衝突的增多導致查詢時間越來越慢。會出現一種惡劣的情況,當雜湊演演算法特別差時;元素總數n和某個槽位數 m 中的 k 相等,如下圖所示

在這種情況下,查詢的時間為 $O(1+a)$ 其中 $O(1)$ 為hash

通過下圖可以得知 Hashtable 與其他類的關係

classDiagram direction BT class Cloneable { <<Interface>> } class Dictionary~K, V~ class Hashtable~K, V~ class Map~K, V~ { <<Interface>> } class Serializable { <<Interface>> } Hashtable~K, V~ ..> Cloneable Hashtable~K, V~ --> Dictionary~K, V~ Hashtable~K, V~ ..> Map~K, V~ Hashtable~K, V~ ..> Serializable

實際上,Hashtable中的每個元素都是一個 Map.Entry<k,v>EntryMap 的集合形式 用來遍歷Map 。Hashtable實現了該介面,Hashtable就是一個集合,不過儲存的是一個一個連結串列。

private static class Entry<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;  
	V value;  
	Entry<K,V> next;
	
	public K getKey(){...}
	public V getValue(){...}
	public V setValue(V value){...}
}

Hashtable有幾個關鍵的欄位需要注意:

private int threshold; // 可容納的極限長度,容量*負載因子
private float loadFactor; // 負載因子 該值預設為0.75

如果把Hashtbale比做桶,負載因子就表明一桶水能裝半桶還是裝滿桶還是裝四分之一桶。
負載因子越大,能裝的水就越多。負載因子總和臨界值配合,臨界值用來表示什麼時候擴容,也就是水裝不下了得換一個大一點的桶裝水。Hashtable每一次擴容都會擴大到原來的兩倍大。

負載因子是對時間和空間的平衡,當負載因子增大空間會比較充足就不需要總是擴容,空間用的較多;如果負載因子小需要不斷擴容,但是空間用的少。

通過一個put方法來了解

下圖簡述了put的流程

計算位置

Hashtable中計算位置特別簡單,就是簡單的除法

Entry<?,?> tab[] = table;
int hash = key.hashCode();  
int index = (hash & 0x7FFFFFFF) % tab.length;

插入元素

首先,Hashtable需要知道當前put操作是更新舊值還是插入新值。如果更新舊值就返回舊值並更新它
下面就是一個不斷查詢連結串列的過程

Entry<K,V> entry = (Entry<K,V>)tab[index];  
for(; entry != null ; entry = entry.next) {  
    if ((entry.hash == hash) && entry.key.equals(key)) {  
        V old = entry.value;  
        entry.value = value;  
        return old;  
    }  
}

如果是插入新值則建立一個 Entry 並插入,這是在容量沒有超過臨界值的情況:

Entry<K,V> e = (Entry<K,V>) tab[index];  
tab[index] = new Entry<>(hash, key, value, e);  
count++;  
modCount++;

當然,如果容量超過臨界值則需要擴容

擴容

if (count >= threshold) {  
    // 擴容,並重新計算每個元素的hash值
    rehash();  

	// 擴容之後插入新值
    tab = table;  
    hash = key.hashCode();  
    index = (hash & 0x7FFFFFFF) % tab.length;  
}

擴容的關鍵是 rehash() 這個方法。該方法也很簡單,只有以下幾個步驟:

  1. 計算新的臨界值
  2. 新臨界值超過最大能接受的容量則不再擴充
  3. 建立一個新table(新的大桶)
  4. 逐個計算hash值並重新裝填table

執行緒安全性

Hashtable是執行緒安全的,主要是通過為每個方法加入一個同步鎖來解決,如put方法

public synchronized V put(K key, V value) {...}

但是這樣效能還是比較低的,同時不能保證組合方法的執行緒安全性。
例如 getremove

public V getAndRemove(Object o){
	V v = get(o);
	remove(o);
	return v;
}

這樣是不能保證執行緒安全的