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方法必須一起重寫。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM