HashMap HashTable和ConcurrentHashMap

HashMap和Hashtable的區別

HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

  1. HashMap幾乎可以等價於Hashtable,除了HashMap是非synchronized的,並可以接受null(HashMap可以接受爲null的鍵值(key)和值(value),而Hashtable則不行)。
  2. HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。
  3. 另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行爲,要看JVM。這條同樣也是Enumeration和Iterator的區別。
  4. 由於Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那麼使用HashMap性能要好過Hashtable。
  5. HashMap不能保證隨着時間的推移Map中的元素次序是不變的。

要注意的一些重要術語:

1) sychronized意味着在一次僅有一個線程能夠更改Hashtable。就是說任何線程要更新Hashtable時要首先獲得同步鎖,其它線程要等到同步鎖被釋放之後才能再次獲得同步鎖更新Hashtable。

2) Fail-safe和iterator迭代器相關。如果某個集合對象創建了Iterator或者ListIterator,然後其它的線程試圖“結構上”更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程可以通過set()方法更改集合對象是允許的,因爲這並沒有從“結構上”更改集合。但是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

3) 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。

我們能否讓HashMap同步?

HashMap可以通過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);

結論

Hashtable和HashMap有幾個主要的不同:線程安全以及速度。僅在你需要完全的線程安全的時候使用Hashtable,而如果你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

 

HashMap和ConcurrentHashMap分享

大家一看到這兩個類就能想到HashMap不是線程安全的,ConcurrentHashMap是線程安全的。除了這些,還知道什麼呢? 

先看一下簡單的類圖: 



從類圖中可以看出來在存儲結構中ConcurrentHashMap比HashMap多出了一個類Segment,而Segment是一個可重入鎖。 
ConcurrentHashMap是使用了鎖分段技術技術來保證線程安全的。 
鎖分段技術:首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。 

屬性說明: 
我們會發現HashMap和Segment裏的屬性值基本是一樣的,因爲Segment的本質上就是一個加鎖的HashMap,下面是每個屬性的意義: 
table:數據存儲區 
size,count: 已存數據的大小 
threshold:table需要擴容的臨界值,等於table的大小*loadFactor 
loadFactor: 裝載因子 
modCount: table結構別修改的次數 

hash算法和table數組長度: 
仔細閱讀HashMap的構造方法的話,會發現他做了一個操作保證table數組的大小是2的n次方。 
如果使用new HashMap(10)新建一個HashMap,你會發現這個HashMap中table數組實際的大小是16,並不是10.
爲什麼要這麼做呢?這就要從HashMap裏的hash和indexFor方法開始說了。 

  1. static int hash(int h) {  
  2.     // This function ensures that hashCodes that differ only by  
  3.     // constant multiples at each bit position have a bounded  
  4.     // number of collisions (approximately 8 at default load factor).  
  5.     h ^= (h >>> 20) ^ (h >>> 12);  
  6.     return h ^ (h >>> 7) ^ (h >>> 4);  
  7. }  
  8.   
  9. /** 
  10.  * Returns index for hash code h. 
  11.  */  
  12. static int indexFor(int h, int length) {  
  13.     return h & (length-1);  
  14. }  
  15.   
  16. int hash = hash(key.hashCode());  
  17. int i = indexFor(hash, table.length);  


HashMap裏的put和get方法都使用了這兩個方法將key散列到table數組上去。 
indexFor方法是通過hash值和table數組的長度-1進行於操作,來確定具體的位置。 
爲什麼要減1呢?因爲數組的長度是2的n次方,減1以後就變成低位的二進制碼都是1,和hash值做與運算的話,就能得到一個小於數組長度的數了。 
那爲什麼對hashCode還要做一次hash操作呢?因爲如果不做hash操作的話,只有低位的值參與了hash的運算,而高位的值沒有參加運算。hash方法是讓高位的數字也參加hash運算。 
假如:數組的長度是16 我們會發現hashcode爲5和53的散列到同一個位置. 
hashcode:53  00000000 00000000 00000000 00110101 
hashcode:5    00000000 00000000 00000000 00000101 
length-1:15     00000000 00000000 00000000 00001111 
只要hashcode值的最後4位是一樣的,那麼他們就會散列到同一個位置。 
hash方法是通過一些位運算符,讓高位的數值也儘可能的參加到運算中,讓它儘可能的散列到table數組上,減少hash衝突。 

ConcurrentHashMap的初始化: 
仔細閱讀ConcurrentHashMap的構造方法的話,會發現是由initialCapacity,loadFactor, concurrencyLevel幾個參數來初始化segments數組的。 
segmentShift和segmentMask是在定位segment時的哈希算法裏需要使用的,讓其能夠儘可能的散列開。 
initialCapacity:ConcurrentHashMap的初始大小 
loadFactor:裝載因子 
concurrencyLevel:預想的併發級別,爲了能夠更好的hash,也保證了concurrencyLevel的值是2的n次方 
segements數組的大小爲concurrencyLevel,每個Segement內table的大小爲initialCapacity/ concurrencyLevel 

ConcurrentHashMap的put和get 

  1. int hash = hash(key.hashCode());  
  2. return segmentFor(hash).get(key, hash);  


可以發現ConcurrentHashMap通過一次hash,兩次定位來找到具體的值的。 
先通過segmentFor方法定位到具體的Segment,再在Segment內部定位到具體的HashEntry,而第二次在Segment內部定位的時候是加鎖的。 
ConcurrentHashMap的hash算法比HashMap的hash算法更復雜,應該是想讓他能夠更好的散列到數組上,減少hash衝突。 

HashMap和Segment裏modCount的區別: 
modCount都是記錄table結構被修改的次數,但是對這個次數的處理上,HashMap和Segment是不一樣的。 
HashMap在遍歷數據的時候,會判斷modCount是否被修改了,如果被修改的話會拋出ConcurrentModificationException異常。 
Segment的modCount在ConcurrentHashMap的containsValue、isEmpty、size方法中用到,ConcurrentHashMap先在不加鎖的情況下去做這些計算,如果發現有Segment的modCount被修改了,會再重新獲取鎖計算。 

HashMap和ConcurrentHashMap的區別: 
如果仔細閱讀他們的源碼,就會發現HashMap是允許插入key和value是null的數據的,而ConcurrentHashMap是不允許key和value是null的。這個是爲什麼呢?ConcurrentHashMap的作者是這麼說的: 
The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if map.get(key) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls. 

爲什麼重寫了equals方法就必須重寫hashCode方法呢? 
絕大多數人都知道如果要把一個對象當作key使用的話,就需要重寫equals方法。重寫了equals方法的話,就必須重寫hashCode方法,否則會出現不正確的結果。那麼爲什麼不重寫hashCode方法就會出現不正確結果了呢?這個問題只要仔細閱讀一下HashMap的put方法,看看它是如何確定一個key是否已存在的就明白了。關鍵代碼: 

  1. int hash = hash(key.hashCode());  
  2. int i = indexFor(hash, table.length);  
  3. for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  4.     Object k;  
  5.     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  6.         V oldValue = e.value;  
  7.         e.value = value;  
  8.         e.recordAccess(this);  
  9.         return oldValue;  
  10.     }  
  11. }  


首先通過key的hashCode來確定具體散列到table的位置,如果這個位置已經有值的話,再通過equals方法判斷key是否相等。 
如果只重寫equals方法而不重寫hashCode方法的話,即使這兩個對象通過equals方法判斷是相等的,但是因爲沒有重寫hashCode方法,他們的hashCode是不一樣的,這樣就會被散列到不同的位置去,變成錯誤的結果了。所以hashCode和equals方法必須一起重寫。 

TreeMap

TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。
TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。
TreeMap 實現了NavigableMap接口,意味着它支持一系列的導航方法。比如返回有序的key集合。
TreeMap 實現了Cloneable接口,意味着它能被克隆。
TreeMap 實現了java.io.Serializable接口,意味着它支持序列化。

TreeMap基於紅黑樹(Red-Black tree)實現。該映射根據其鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator 進行排序,具體取決於使用的構造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。


發佈了32 篇原創文章 · 獲贊 40 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章