HashTable的故事----Jdk源碼解讀

HashTable的故事

很早之前,在講HashMap的時候,我們就說過hash是散列,把...弄碎的意思。hashtable中的hash也是這個意思,而table呢,是指數據表格,也就是說hashtable的本意是指,一份被數據被打散,分散在各處的數據表格。

HashTable,作爲jdk中,極早提供的容器類(jdk1.0),同時是支持數據併發的類,其在項目中的使用卻並不是很廣泛。在我所經歷的項目中,開發人員往往喜歡使用hashMap然後再通過鎖,創造出線程安全的環境。即使是後來推出concurrentHashMap,其使用的地方也並沒有特別廣泛。究其原因,我覺得是由於開發人員對於其他hash容器並不熟悉。更願意使用已有的較爲熟悉的hash容器,即使他們在此處的應用比較費事。

好了,廢話不多說,我們直接開始進入正題吧:

hashTable繼承自dic類,同時實現了map接口和Cloneable、Serializable兩個接口,代表該類是可複製、序列化的類。

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

ps:dic類和map類較爲相似,是一個抽象的hash映射類,包含了一些簡單的空方法和接口。

private transient Entry<?,?>[] table;

瞬時數組變量,它就是hashtable中,最核心的數據存儲區域。

 

    /**
     * The total number of entries in the hash table.
     */
    private transient int count;

數組長度,不知道大家發現沒有,jdk非常喜歡用一個獨立變量來表示容器中數據的大小,而不是每次返回核心數據的size或length。

 

閾值,這個之前專門強調過,這裏簡單說下,他是容積和負載因子的乘積,表示的含義是當前容器中,能表現出較好性能的數據量上限。超過這個上限時,容器的性能將會有比較大的下降。注意容積和閾值是有區別的。

threshold  ['θrɛʃhold]  n. 入口;門檻;開始;極限;臨界值 

   private int threshold;

負載因子,是用來設定當前容器中,元素的填充率的。

你可以理解成容器是一個城市,這個城市中最佳入住率的一個上限是負載因子。這個城市的入住用戶最佳的數目,就是他的閾值。

    /**
     * The load factor for the hashtable.
     *
     * @serial
     */
    private float loadFactor;

接下來是modCount ,這個變量的意義是,記錄hashtable中,被修改的次數(包括增、刪、改)三個操作的。而其用途呢,是未來被用作判定快速失敗時(fail-fast)的依據數據。關於快速失敗,這個我會在下邊講到。大家這裏只要知道modCount這個變量的表示的含義是什麼就可以。

    private transient int modCount = 0;

然後是版本序列號

    private static final long serialVersionUID = 1421746759512286392L;

接着是構造函數,參數分別爲初始容積和負載因子。

函數內會首先判斷初始容積和負載因子是否爲正數。

接着如果初始容積爲0,則賦予默認值1.也就是說,真實的容積至少都要爲1。

接着對table賦予初始值,一個長度爲初始容積大小的Entry數組。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )接着計算

閾值=(初始容積和負載因子的乘積),(當前系統中最大的數組長度+1),二者的最小值。

也就是說閾值不能超過數組的最大長度+1。這裏注意一個isNaN()方法,是個很有意思的方法,研究該方法的源碼後,你會覺得很有意思。這個我會在以後的文章中講到。

public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

只要初始容積的構造函數,負載因子默認爲0.75

    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

無參的構造函數,初始容積使用爲11,負載因子爲0.75

    public Hashtable() {
        this(11, 0.75f);
    }

不知道大家發現沒,儘管提供了一個可能的,但是jdk的源碼往往系統提供多個,應用於不同場景的接口,這些接口往往其實只是對自身其他接口的一個適配。但是對於調用者來說,這樣卻很舒服。

 

接着是最後一個構造函數,參數爲一個map,map的k,v分別繼承自hashTable中的K,V.

函數首先調用一遍通用的構造函數,負載因子爲0.75。初始容積爲map長度的兩倍以及默認的11,二者的較大值。也就是說對於初始容積來說,最小都要取到11。

接着調用putAll方法,將map中的數據添加到HashTable中。

    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }

size()方法,方法採用同步機制,返回count變量。由於容器中並不是所有的元素都佔滿了數據,所以直接用變量返回值的速度和效率會更高點。同時由於count會隨時變動,這裏採用同步方法的形式進行線程保護。

    public synchronized int size() {
        return count;
    }

isEmpyt,判斷當前數組是否爲空,與size()方法一致。

    public synchronized boolean isEmpty() {
        return count == 0;
    }

keys,elements方法,分別返回返回hashTable中所有的key和value的枚舉集合。

這裏KEYS,VALUES爲靜態int常量。getEnumeration在下文中會提到。另外與前邊的方法相同,這裏也是對整個方法進行同步加鎖。

    public synchronized Enumeration<K> keys() {
        return this.<K>getEnumeration(KEYS);
    }

    public synchronized Enumeration<V> elements() {
        return this.<V>getEnumeration(VALUES);
    }

 接着是contains方法,方法意義不再贅述。

實現邏輯,首先判斷value是否爲null,如果爲null則直接拋出空引用。

接着將table變量賦值給tab臨時變量。然後循環tab,依次取出tab中的entry,以及entry的後繼元素。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )如果元素的value equals()判斷等於參數value,則直接返回true。整個方法結束後,爲發現,則會返回false。同時方法本身也是加同步鎖進行線程安全保護。

    public synchronized boolean contains(Object value) {
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        for (int i = tab.length ; i-- > 0 ;) {
            for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
                if (e.value.equals(value)) {
                    return true;
                }
            }
        }
        return false;
    }

接着是實現map接口的抽象方法,只是對contains方法進行了一層封裝。

    public boolean containsValue(Object value) {
        return contains(value);
    }

接着是線程同步方法:containsKey,方法含義不贅述,邏輯如下:

設定臨時變量並賦值table。取出key的hashCode。注意這裏並沒有判定key是否爲null。

而前文中的value則是判定的。這是由於value是作爲equals方法的參數的。即使是null也無法被發現,但是判定一個映射的value爲null表示的真的爲null還是沒有映射到,這很歧義,所以乾脆直接拋出異常。回到正文,根據hashCode計算出其在table數組中的索引。其實就是取低8位數字然後除以數組length取餘數。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )

接着依次循環table該索引的後繼元素,判定是否equals()相等。如果有則返回true。如果始終沒有找到,則返回false。

    public synchronized boolean containsKey(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return true;
            }
        }
        return false;
    }

get()方法,與containsKey方法的邏輯是一致的。不同點是,在返回結果是,如果確實存在該key,則返回對應的value,否則返回null。

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

接着是前文提到的數組最大數字常亮。這裏注意看參數的註釋。部分虛擬機是設定數組的長度限制的。如果超出,可能會導致OOM異常

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

接着是rehash方法。這個方法是一個受保護方法。會在接下來的,hashtable添加元素的場景中被調用。他的作用呢,就是重新申請一塊大小合適的內存。然後將鍵值元素重新安置到這塊元素中。

那麼就需要兩個步驟。

1、計算新內存的大小。

2、計算元素在新table中的位置。

    先看代碼:

 1     protected void rehash() {
 2         int oldCapacity = table.length;
 3         Entry<?,?>[] oldMap = table;
 4 
 5         // overflow-conscious code
 6         int newCapacity = (oldCapacity << 1) + 1;
 7         if (newCapacity - MAX_ARRAY_SIZE > 0) {
 8             if (oldCapacity == MAX_ARRAY_SIZE)
 9                 // Keep running with MAX_ARRAY_SIZE buckets
10                 return;
11             newCapacity = MAX_ARRAY_SIZE;
12         }
13         Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
14 
15         modCount++;
16         threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
17         table = newMap;
18 
19         for (int i = oldCapacity ; i-- > 0 ;) {
20             for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
21                 Entry<K,V> e = old;
22                 old = old.next;
23 
24                 int index = (e.hash & 0x7FFFFFFF) % newCapacity;
25                 e.next = (Entry<K,V>)newMap[index];
26                 newMap[index] = e;
27             }
28         }
29     }

代碼中會首先獲取舊table的長度oldCapacity 。然後oldCapacity 乘以2再加1.算出新table的長度newCapacity 。

接着判斷newCapacity 是否超出了hashtable所能設定的最大值:MAX_ARRAY_SIZE。如果超出,則判斷oldCapacity 是否已經等於最大值。如果已經等於,則認定,當前hashtable的長度已經到達所允許的上限。無法再繼續擴容。則直接返回。

否則將MAX_ARRAY_SIZE賦值給newCapacity 。作爲新的長度。也就是說rehash在大小允許的情況下,一般會翻倍擴容。但是如果翻倍後長度超出上限,則以上限大小作爲擴容後新的大小。

接着以newCapacity 作爲長度,new出一個Entry數組,作爲新的table元素存放容器。

modCount自加1。

接着計算閾值:newCapacity 乘以負載因子和MAX_ARRAY_SIZE+1 取較小值。注意這裏負載因子是可以大於1的。因此newCapacity 乘以負載因子,式可以大於MAX_ARRAY_SIZE的。

接着就是計算舊有table中的鍵值元素在新table中的位置了:這裏使用的是雙層循環,外層依次遍歷Entry主數組上的元素。如果entry[i]不等於null值,則將該元素及其後繼元素依次計算出新的位置,然後插入到主數組上的對應位置。同時將主數組中原來位置的元素。作爲新放置元素的後繼。也就是每個新元素,插在每個對應位置的鏈表最前側。至於爲什麼不放在這個對應鏈表的最後位置。其實很簡單,因爲這是一個鏈式存儲結構,需要依次遍歷每個元素,才能找到隊尾的元素。

接着是添加元素的私有方法addEntry。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )

首先是modCount自加1.

接着如果當前table的數量已經超過了閾值,那麼就進行一次rehash,接着根據key的hashCode計算出當前鍵值對的輸入索引。接着取出table對應索引位置的元素一同做出一個新的Entry元素放在這個對應索引的位置上。(這裏要注意後續的entry 的構造方法)同時count數目自加1。這裏需要注意的是,當前數目如果已經超過閾值,前邊講到的rehash是不一定會重新做出新數組的(length超過了MAX_ARRAY_SIZE的限制時)很多人在理解這裏的時候,就認定只要count超過閾值就一定會重新分配table內存的地址,這個理解是存在問題的。

 1     private void addEntry(int hash, K key, V value, int index) {
 2         modCount++;
 3 
 4         Entry<?,?> tab[] = table;
 5         if (count >= threshold) {
 6             // Rehash the table if the threshold is exceeded
 7             rehash();
 8 
 9             tab = table;
10             hash = key.hashCode();
11             index = (hash & 0x7FFFFFFF) % tab.length;
12         }
13 
14         // Creates the new entry.
15         @SuppressWarnings("unchecked")
16         Entry<K,V> e = (Entry<K,V>) tab[index];
17         tab[index] = new Entry<>(hash, key, value, e);
18         count++;
19     }

接着是put方法。這個方法是hashtable非常常用的一個public方法。方法本身是一個同方法。在方法中對於參數value和key有邏輯:如果爲null時,均會報出空引用異常。

 1     public synchronized V put(K key, V value) {
 2         // Make sure the value is not null
 3         if (value == null) {
 4             throw new NullPointerException();
 5         }
 6 
 7         // Makes sure the key is not already in the hashtable.
 8         Entry<?,?> tab[] = table;
 9         int hash = key.hashCode();
10         int index = (hash & 0x7FFFFFFF) % tab.length;
11         @SuppressWarnings("unchecked")
12         Entry<K,V> entry = (Entry<K,V>)tab[index];
13         for(; entry != null ; entry = entry.next) {
14             if ((entry.hash == hash) && entry.key.equals(key)) {
15                 V old = entry.value;
16                 entry.value = value;
17                 return old;
18             }
19         }
20 
21         addEntry(hash, key, value, index);
22         return null;
23     }

接着算出key所應該對應的主數組的索引。循環遍歷出該數組元素所對應的隊列(tab[index]),如果元素的hash值等於新添加元素的hash,同時entry的key等於(equals)key方法。則直接替換這個entry的value爲參數傳入的value,與此同時返回舊old。

如果整個循環都發現沒有,則說明當前hashtable其實並不存在該參數key,則調用剛纔說的addEntry方法,將參數key value,及對應的索引傳進去。這裏注意put方法爲同步公有方法,而addEntry爲私有非同步方法,這裏是否存在線程安全問題呢?(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )其實並不存在,這是由於addEntry儘管會操作全局變量主數組,但是addEntry方法只會被put方法調用。而凡是調用put方法的線程均需要先拿到this變量鎖。儘管再次進去無同步的addEntry的方法區,當前線程仍然持有this變量鎖,其他線程若想操作全局變量主數組,仍然需要等待全局鎖的釋放纔可以。

接着是remove方法。該方法邏輯如下:首選根據key值計算出元素所對應主數組中的索引位置。然後依次循環主數組下該索引對應元素的後繼元素。判斷該元素的hash是否等於key參數的hash,以及元素是否equels參數key。如果相等,則將該元素從隊列中抹除。同時hashtable的長度count 減1,同時modCount值也自加1。如果循環結束仍未找到合適的元素與參數key相等,則返回null

 

 1     public synchronized V remove(Object key) {
 2         Entry<?,?> tab[] = table;
 3         int hash = key.hashCode();
 4         int index = (hash & 0x7FFFFFFF) % tab.length;
 5         @SuppressWarnings("unchecked")
 6         Entry<K,V> e = (Entry<K,V>)tab[index];
 7         for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
 8             if ((e.hash == hash) && e.key.equals(key)) {
 9                 modCount++;
10                 if (prev != null) {
11                     prev.next = e.next;
12                 } else {
13                     tab[index] = e.next;
14                 }
15                 count--;
16                 V oldValue = e.value;
17                 e.value = null;
18                 return oldValue;
19             }
20         }
21         return null;
22     }

接着是putAll方法,該方法會將map集合中的對象,採用foreach的形式,依次調用put方法添加到hashtable集合中。

    public synchronized void putAll(Map<? extends K, ? extends V> t) {
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
            put(e.getKey(), e.getValue());
    }

接着是clear方法。該方法首先將modCount加1.接着循環主數組中的所有元素,然後依次對這些元素置於null。然後設定count元素爲0。這裏有一點需要注意,即clear時,只清理了主數組中的元素。對於主數組中對應元素的後繼列表,則採用不予理會的態度。等待GC來回收掉。

1     public synchronized void clear() {
2         Entry<?,?> tab[] = table;
3         modCount++;
4         for (int index = tab.length; --index >= 0; )
5             tab[index] = null;
6         count = 0;
7     }

接着是clone方法。該方法會克隆一個自身對象的副本。此方法會克隆出一個空的hashtable。然後將主數組中的所有元素克隆一遍,放置到克隆對象的對應位置上。注意在克隆元素的時候,會將元素的後繼隊列元素,依次的克隆下去。接着初始化克隆對象的其他變量:置空keyset、entryset、values對象,設置modCount爲0。

 1     public synchronized Object clone() {
 2         try {
 3             Hashtable<?,?> t = (Hashtable<?,?>)super.clone();
 4             t.table = new Entry<?,?>[table.length];
 5             for (int i = table.length ; i-- > 0 ; ) {
 6                 t.table[i] = (table[i] != null)
 7                     ? (Entry<?,?>) table[i].clone() : null;
 8             }
 9             t.keySet = null;
10             t.entrySet = null;
11             t.values = null;
12             t.modCount = 0;
13             return t;
14         } catch (CloneNotSupportedException e) {
15             // this shouldn't happen, since we are Cloneable
16             throw new InternalError(e);
17         }
18     }

然後是重寫的tostring方法。這個方法邏輯也很簡單,就是依次遍歷元素。最後生成一個類似於{“key1”=”value1”,“key2”=”value2”}的結構。有趣的是這裏需要調用key.tostring,倘若key是當前hashtable自己的話,就直接使用“(this map)”字符串。防止出現無限遞歸。

 1     public synchronized String toString() {
 2         int max = size() - 1;
 3         if (max == -1)
 4             return "{}";
 5 
 6         StringBuilder sb = new StringBuilder();
 7         Iterator<Map.Entry<K,V>> it = entrySet().iterator();
 8 
 9         sb.append('{');
10         for (int i = 0; ; i++) {
11             Map.Entry<K,V> e = it.next();
12             K key = e.getKey();
13             V value = e.getValue();
14             sb.append(key   == this ? "(this Map)" : key.toString());
15             sb.append('=');
16             sb.append(value == this ? "(this Map)" : value.toString());
17 
18             if (i == max)
19                 return sb.append('}').toString();
20             sb.append(", ");
21         }
22     }

到這裏hashtable的主要邏輯就已經都介紹完了。其餘還包括一些keyset、valueSet的內部類、以及replaceAll、putIfAbsent等封裝方法。由於代碼邏輯簡單,數量較大,這裏就不一一列舉了。

總結:

1、Hashtable包括tostirng等方法在,幾乎所有對外api方法都是同步保護的,這就是爲什麼很多人認爲hashtable線程安全的原因。而在基礎上,對於同步方法所調用的private方法,則大多采用非同步的形式。因爲這些方法,往往只有一個public方法可以調用,這樣就做到了在安全的基礎上可以更快執行代碼。

2、hashtable的內部結構大致如下,和早前的hashmap很像:

3、關於元素的取值,hashtable不允許key和value取值爲null。所以get時,發現爲null,即說明key元素不存在。同時hashtable在擴容是採用的是乘2加1的方式。這與有些容器直接乘2有所區別。

4、關於變量modCount的使用。我們可以看到這個方法中,每次在發生增刪改的時候都會出現modCount++的動作。而modcount可以理解爲是當前hashtable的狀態。每發生一次操作,狀態就向前走一步。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )設置這個狀態,主要是由於hashtable等容器類在迭代時,判斷數據是否過時時使用的。儘管hashtable採用了原生的同步鎖來保護數據安全。但是在出現迭代數據的時候,則無法保證邊迭代,邊正確操作。於是使用這個值來標記狀態。一旦在迭代的過程中狀態發生了改變,則會快速拋出一個異常,終止迭代行爲(所以這種錯誤也叫做快速失敗fail—fast):

            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

由於工作中存在一些變動,所以這篇文章拖了很久才寫完。在寫的過程中,發現越後邊越細。越寫越發現自己離王垠所寫的編程學習越遠(可搜索《如何掌握所有的程序語言》)。因爲最後認爲很多源碼邏輯簡單冗餘也就不再贅述了,這也是後續我對java及其它技術學習以及博客總結的一個指向吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章