2017年的秋招完全結束了,感受Java上面的最多見的集合相關的問題就是hash……系列和一些經常使用併發集合和隊列,堆等結合算法一塊兒考察,不徹底統計,本人經歷:前後百度、惟品會、58同城、新浪微博、趣分期、美團點評等都在一、2……面的時候被問過無數次,都問吐了&_&,其餘公司筆試的時候,但凡是有Java的題,都有集合相關考點,尤爲hash表……如今總結下。面試
2016-12-15 更新:Java 8 對 HashMap 的改進算法
若是說Java的hashmap是數組+鏈表,那麼JDK 8以後就是數組+鏈表+紅黑樹組成了hashmap。以前的實現機制和原理在下面12-12期整理過,此次只說下新加的紅黑樹機制。數組
在以前談過,若是hash算法很差,會使得hash表蛻化爲順序查找,即便負載因子和hash算法優化再多,也沒法避免出現鏈表過長的情景(這個概論雖然很低),因而在JDK1.8中,對hashmap作了優化,引入紅黑樹。具體原理就是當hash表中每一個桶附帶的鏈表長度默認超過8時,鏈表就轉換爲紅黑樹結構,提升HashMap的性能,由於紅黑樹的增刪改是O(logn),而不是O(n)。安全
紅黑樹的具體原理和實現之後再總結。數據結構
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
封裝了一個final方法,裏面用到一個常量,具體用處看源碼:多線程
static final int TREEIFY_THRESHOLD = 8;
下面是具體源代碼註釋:併發
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 if ((tab = table) == null || (n = tab.length) == 0) // 首先判斷hash表是不是空的,若是空,則resize擴容 5 n = (tab = resize()).length; 6 if ((p = tab[i = (n - 1) & hash]) == null) // 經過key計算獲得hash表下標,若是下標處爲null,就新建鏈表頭結點,在方法最後插入便可 7 tab[i] = newNode(hash, key, value, null); 8 else { // 若是下標處已經存在節點,則進入到這裏 9 Node<K,V> e; K k; 10 if (p.hash == hash && 11 ((k = p.key) == key || (key != null && key.equals(k)))) // 先看hash表該處的頭結點是否和key同樣(hashcode和equals比較),同樣就更新 12 e = p; 13 else if (p instanceof TreeNode) // hash表頭結點和key不同,則判斷節點是否是紅黑樹,是紅黑樹就按照紅黑樹處理 14 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 15 else { // 若是不是紅黑樹,則按照以前的hashmap原理處理 16 for (int binCount = 0; ; ++binCount) { // 遍歷鏈表 17 if ((e = p.next) == null) { 18 p.next = newNode(hash, key, value, null); 19 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st (原jdk註釋) 顯然當鏈表長度大於等於7的時候,也就是說大於8的話,就轉化爲紅黑樹結構,針對紅黑樹進行插入(logn複雜度) 20 treeifyBin(tab, hash); 21 break; 22 } 23 if (e.hash == hash && 24 ((k = e.key) == key || (key != null && key.equals(k)))) 25 break; 26 p = e; 27 } 28 } 29 if (e != null) { // existing mapping for key 30 V oldValue = e.value; 31 if (!onlyIfAbsent || oldValue == null) 32 e.value = value; 33 afterNodeAccess(e); 34 return oldValue; 35 } 36 } 37 ++modCount; 38 if (++size > threshold) // 若是超過容量,即擴容 39 resize(); 40 afterNodeInsertion(evict); 41 return null; 42 }
resize是新的擴容方法,以前談過,擴容原理是使用新的(2倍舊長度)的數組代替,把舊數組的內容放到新數組,須要從新計算hash和計算hash表的位置,很是耗時,可是自從 JDK 1.8 對hashmap 引入了紅黑樹,它和以前的擴容方法有了改進。app
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 int oldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) { 7 if (oldCap >= MAXIMUM_CAPACITY) { 8 threshold = Integer.MAX_VALUE; 9 return oldTab; 10 } 11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 12 oldCap >= DEFAULT_INITIAL_CAPACITY) // 若是長度沒有超過最大值,則擴容爲2倍的關係 13 newThr = oldThr << 1; // double threshold 14 } 15 else if (oldThr > 0) // initial capacity was placed in threshold 16 newCap = oldThr; 17 else { // zero initial threshold signifies using defaults 18 newCap = DEFAULT_INITIAL_CAPACITY; 19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 20 } 21 if (newThr == 0) { 22 float ft = (float)newCap * loadFactor; 23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 24 (int)ft : Integer.MAX_VALUE); 25 } 26 threshold = newThr; 27 @SuppressWarnings({"rawtypes","unchecked"}) 28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 29 table = newTab; 30 if (oldTab != null) { // 進行新舊元素的轉移過程 31 for (int j = 0; j < oldCap; ++j) { 32 Node<K,V> e; 33 if ((e = oldTab[j]) != null) { 34 oldTab[j] = null; 35 if (e.next == null) 36 newTab[e.hash & (newCap - 1)] = e; 37 else if (e instanceof TreeNode) 38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 39 else { // preserve order(原註釋) 若是不是紅黑樹的狀況這裏改進了,沒有rehash的過程,以下分別記錄鏈表的頭尾 40 Node<K,V> loHead = null, loTail = null; 41 Node<K,V> hiHead = null, hiTail = null; 42 Node<K,V> next; 43 do { 44 next = e.next; 45 if ((e.hash & oldCap) == 0) { 46 if (loTail == null) 47 loHead = e; 48 else 49 loTail.next = e; 50 loTail = e; 51 } 52 else { 53 if (hiTail == null) 54 hiHead = e; 55 else 56 hiTail.next = e; 57 hiTail = e; 58 } 59 } while ((e = next) != null); 60 if (loTail != null) { 61 loTail.next = null; 62 newTab[j] = loHead; 63 } 64 if (hiTail != null) { 65 hiTail.next = null; 66 newTab[j + oldCap] = hiHead; 67 } 68 } 69 } 70 } 71 } 72 return newTab; 73 }
由於有這樣一個特色:好比hash表的長度是16,那麼15對應二進制是:ide
0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15函數
擴容以前有兩個key,分別是k1和k2:
k1的hash:
0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15
k2的hash:
0000 0000, 0000 0000, 0000 0000, 0001 1111 = 15
hash值和15模獲得:
k1:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15
k2:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15
擴容以後表長對應爲32,則31二進制:
0000 0000, 0000 0000, 0000 0000, 0001 1111 = 31
從新hash以後獲得:
k1:0000 0000, 0000 0000, 0000 0000, 0000 1111 = 15
k2:0000 0000, 0000 0000, 0000 0000, 0001 1111 = 31 = 15 + 16
觀察發現:若是擴容後新增的位是0,那麼rehash索引不變,不然纔會改變,而且變爲原來的索引+舊hash表的長度,故咱們只需看原hash表長新增的bit是1仍是0,若是是0,索引不變,若是是1,索引變成原索引+舊錶長,根本不用像JDK 7 那樣rehash,省去了從新計算hash值的時間,並且新增的bit是0仍是1能夠認爲是隨機的,所以resize的過程,還能均勻的把以前的衝突節點分散。
故JDK 8對HashMap的優化是很是到位的。
以下是以前整理的舊hash的實現機制和原理,並和jdk古老的hashtable作了比較。
2016-12-12 整理jdk 1.8以前的HashMap實現:
先上圖
Java 中有四種常見的Map實現——HashMap, TreeMap, Hashtable和LinkedHashMap:
本文重點總結HashMap,HashMap是基於哈希表實現的,每個元素是一個key-value對,其內部經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。
HashMap是非線程安全的,只用於單線程環境下,多線程環境下能夠採用concurrent併發包下的concurrentHashMap。
HashMap 實現了Serializable接口,所以它支持序列化。
HashMap還實現了Cloneable接口,故能被克隆。
紫色部分即表明哈希表自己(實際上是一個數組),數組的每一個元素都是一個單鏈表的頭節點,鏈表是用來解決hash地址衝突的,若是不一樣的key映射到了數組的同一位置處,就將其放入單鏈表中保存。
這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是建立哈希表時的容量(默認爲16),加載因子是哈希表當前key的數量和容量的比值,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表提早進行 resize 操做(即擴容)。若是加載因子越大,對空間的利用更充分,可是查找效率會下降(鏈表長度會愈來愈長);若是加載因子過小,那麼表中的數據將過於稀疏(不少空間還沒用,就開始擴容了),嚴重浪費。
JDK開發者規定的默認加載因子爲0.75,由於這是一個比較理想的值。另外,不管指定初始容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的冪次方,且最大值不能超過2的30次方。
1 // 獲取key對應的value 2 public V get(Object key) { 3 if (key == null) 4 return getForNullKey(); 5 // 獲取key的hash值 6 int hash = hash(key.hashCode()); 7 // 在「該hash值對應的鏈表」上查找「鍵值等於key」的元素 8 for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { 9 Object k; 10 // 判斷key是否相同 11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 12 return e.value; 13 } 14 // 沒找到則返回null 15 return null; 16 } 17 18 // 獲取「key爲null」的元素的值,HashMap將「key爲null」的元素存儲在table[0]位置,但不必定是該鏈表的第一個位置! 20 private V getForNullKey() { 21 for (Entry<K, V> e = table[0]; e != null; e = e.next) { 22 if (e.key == null) 23 return e.value; 24 } 25 return null; 26 }
首先,若是key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中,固然不必定是存放在頭結點table[0]中。若是key不爲null,則先求的key的hash值,根據hash值找到在table中的索引,在該索引對應的單鏈表中查找是否有鍵值對的key與目標key相等,有就返回對應的value,沒有則返回null。
1 // 將「key-value」添加到HashMap中 2 public V put(K key, V value) { 3 // 若「key爲null」,則將該鍵值對添加到table[0]中。 4 if (key == null) 5 return putForNullKey(value); 6 // 若「key不爲null」,則計算該key的哈希值,而後將其添加到該哈希值對應的鏈表中。 7 int hash = hash(key.hashCode()); 8 int i = indexFor(hash, table.length); 9 for (Entry<K, V> e = table[i]; e != null; e = e.next) { 10 Object k; 11 // 若「該key」對應的鍵值對已經存在,則用新的value取代舊的value。而後退出! 12 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 13 V oldValue = e.value; 14 e.value = value; 15 e.recordAccess(this); 16 return oldValue; 17 } 18 } 19 20 // 若「該key」對應的鍵值對不存在,則將「key-value」添加到table中 21 modCount++; 22 // 將key-value添加到table[i]處 23 addEntry(hash, key, value, i); 24 return null; 25 }
若是key爲null,則將其添加到table[0]對應的鏈表中,若是key不爲null,則一樣先求出key的hash值,根據hash值得出在table中的索引,然後遍歷對應的單鏈表,若是單鏈表中存在與目標key相等的鍵值對,則將新的value覆蓋舊的value,且將舊的value返回,若是找不到與目標key相等的鍵值對,或者該單鏈表爲空,則將該鍵值對插入到單鏈表的頭結點位置(每次新插入的節點都是放在頭結點的位置),該操做是有addEntry方法實現的,它的源碼以下:
// 新增Entry。將「key-value」插入指定位置,bucketIndex是位置索引。 void addEntry(int hash, K key, V value, int bucketIndex) { // 保存「bucketIndex」位置的值到「e」中 Entry<K, V> e = table[bucketIndex]; // 設置「bucketIndex」位置的元素爲「新Entry」, // 設置「e」爲「新Entry的下一個節點」 table[bucketIndex] = new Entry<K, V>(hash, key, value, e); // 若HashMap的實際大小 不小於 「閾值」,則調整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); }
注意這裏倒數第三行的構造方法,將key-value鍵值對賦給table[bucketIndex],並將其next指向元素e,這便將key-value放到了頭結點中,並將以前的頭結點接在了它的後面。該方法也說明,每次put鍵值對的時候,老是將新的該鍵值對放在table[bucketIndex]處(即頭結點處)。兩外注意最後兩行代碼,每次加入鍵值對時,都要判斷當前已用的槽的數目是否大於等於閥值(容量*加載因子),若是大於等於,則進行擴容,將容量擴爲原來容量的2倍。
static int indexFor(int h, int length) { return h & (length-1); }
由於容量初始仍是設定都會轉化爲2的冪次。故可使用高效的位與運算替代模運算。下面會解釋緣由。
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
JDK 的 HashMap 使用了一個 hash 方法對hash值使用位的操做,使hash值的計算效率很高。爲何這樣作?主要是由於若是直接使用hashcode值,那麼這是一個int值(8個16進制數,共32位),int值的範圍正負21億多,可是hash表沒有那麼長,通常好比初始16,天然散列地址須要對hash表長度取模運算,獲得的餘數纔是地址下標。假設某個key的hashcode是0AAA0000,hash數組長默認16,若是不通過hash函數處理,該鍵值對會被存放在hash數組中下標爲0處,由於0AAA0000 & (16-1) = 0。過了一下子又存儲另一個鍵值對,其key的hashcode是0BBB0000,獲得數組下標依然是0,這就說明這是個實現得不好的hash算法,由於hashcode的1位全集中在前16位了,致使算出來的數組下標一直是0。因而明明key相差很大的鍵值對,卻存放在了同一個鏈表裏,致使之後查詢起來比較慢(蛻化爲了順序查找)。故JDK的設計者使用hash函數的若干次的移位、異或操做,把hashcode的「1位」變得「鬆散」,很是巧妙。
前面說了,hashmap的構造器裏指明瞭兩個對於理解HashMap比較重要的兩個參數 int initialCapacity, float loadFactor,這兩個參數會影響HashMap效率,HashMap底層採用的散列數組實現,利用initialCapacity這個參數咱們能夠設置這個數組的大小,也就是散列桶的數量,可是若是須要Map的數據過多,在不斷的add以後,這些桶可能都會被佔滿,這是有兩種策略,一種是不改變Capacity,由於即便桶佔滿了,咱們仍是能夠利用每一個桶附帶的鏈表增長元素。可是這有個缺點,此時HaspMap就退化成爲了LinkedList,使get和put方法的時間開銷上升,這是就要採用另外一種方法:增長Hash桶的數量,這樣get和put的時間開銷又回退到近於常數複雜度上。Hashmap就是採用的該方法。
1 // 從新調整HashMap的大小,newCapacity是調整後的單位 2 void resize(int newCapacity) { 3 Entry[] oldTable = table; 4 int oldCapacity = oldTable.length; 5 if (oldCapacity == MAXIMUM_CAPACITY) { 6 threshold = Integer.MAX_VALUE; 7 return; 8 } 9 10 // 新建一個HashMap,將「舊HashMap」的所有元素添加到「新HashMap」中, 11 // 而後,將「新HashMap」賦值給「舊HashMap」。 12 Entry[] newTable = new Entry[newCapacity]; 13 transfer(newTable); 14 table = newTable; 15 threshold = (int) (newCapacity * loadFactor); 16 }
很明顯,是重新建了一個HashMap的底層數組,長度爲原來的兩倍,然後調用transfer方法,將舊HashMap的所有元素添加到新的HashMap中(要從新計算元素在新的數組中的索引位置)。transfer方法的源碼以下:
1 // 將HashMap中的所有元素都添加到newTable中 2 void transfer(Entry[] newTable) { 3 Entry[] src = table; 4 int newCapacity = newTable.length; 5 for (int j = 0; j < src.length; j++) { 6 Entry<K, V> e = src[j]; 7 if (e != null) { 8 src[j] = null; 9 do { 10 Entry<K, V> next = e.next; 11 int i = indexFor(e.hash, newCapacity); 12 e.next = newTable[i]; 13 newTable[i] = e; 14 e = next; 15 } while (e != null); 16 } 17 } 18 }
很明顯,擴容是一個至關耗時的操做,由於它須要從新計算這些元素在新的數組中的位置並進行復制處理。所以,咱們在用HashMap時,最好能提早預估下HashMap中元素的個數,這樣有助於提升HashMap的性能。
由於效率問題,JDK採用預處理法,這時前面說的loadFactor就派上了用場,當size > initialCapacity * loadFactor,hashmap內部resize方法就被調用,使得從新擴充hash桶的數量,在目前的實現中,是增長一倍,這樣就保證當你真正想put新的元素時效率不會明顯降低。因此通常狀況下HashMap並不存在鍵值放滿的狀況。固然並不排除極端狀況,好比設置的JVM內存用完了,或者這個HashMap的Capacity已經達到了MAXIMUM_CAPACITY(目前的實現是2^30)。
initialCapacity的默認值是16,有些人可能會想若是內存足夠,是否是能夠將initialCapacity設大一些,即便用不了這麼大,就可避免擴容致使的效率的降低,反正不管initialCapacity大小,咱們使用的get和put方法都是常數複雜度的。這麼說沒什麼不對,可是可能會忽略一點,實際的程序可能不只僅使用get和put方法,也有可能使用迭代器,如initialCapacity容量較大,那麼會使迭代器效率下降。因此理想的狀況仍是在使用HashMap前估計一下數據量。
加載因子默認值是0.75,是JDK權衡時間和空間效率以後獲得的一個相對優良的數值。若是這個值過大,雖然空間利用率是高了,可是對於HashMap中的一些方法的效率就降低了,包括get和put方法,會致使每一個hash桶所附加的鏈表增加,影響存取效率。若是比較小,除了致使空間利用率較低外沒有什麼壞處,只要有的是內存,畢竟如今大多數人把時間看的比空間重要。可是實際中仍是不多有人會將這個值設置的低於0.5。
若是key爲null,則直接從哈希表的第一個位置table[0]對應的鏈表上查找。記住,key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中。
JDK使用了鏈地址法,hash表的每一個元素又分別連接着一個單鏈表,元素爲頭結點,若是不一樣的key映射到了相同的下標,那麼就使用頭插法,插入到該元素對應的鏈表。
咱們通常對哈希表的散列很天然地會想到用hash值對length取模(即除留餘數法),HashTable就是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,且hashtable直接使用了hashcode值,沒有從新計算。
HashMap中則經過 h&(length-1) 的方法來代替取模,其中h是key的hash值,一樣實現了均勻的散列,但效率要高不少,這也是HashMap對Hashtable的一個改進。
接下來,咱們分析下爲何哈希表的容量必定要是2的整數次冪。
首先,length爲2的整數次冪的話,h&(length-1) 在數學上就至關於對length取模,這樣便保證了散列的均勻,同時也提高了效率;
其次,length爲2的整數次冪的話,則必定爲偶數,那麼 length-1 必定爲奇數,奇數的二進制的最後一位是1,這樣便保證了 h&(length-1) 的最後一位可能爲0,也可能爲1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣即可以保證散列的均勻,而若是length爲奇數的話,很明顯 length-1 爲偶數,它的最後一位是0,這樣 h&(length-1) 的最後一位確定爲0,即只能爲偶數,這樣致使了任何hash值都只會被散列到數組的偶數下標位置上,浪費了一半的空間,所以length取2的整數次冪,是爲了使不一樣hash值發生碰撞的機率較小,這樣就能使元素在哈希表中均勻地散列。
HashTable一樣是基於哈希表實現的,其實相似HashMap,只不過有些區別,HashTable一樣每一個元素是一個key-value對,其內部也是經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。
HashTable比較古老, 是JDK1.0就引入的類,而HashMap 是 1.2 引進的 Map 的一個實現。
HashTable 是線程安全的,能用於多線程環境中。Hashtable一樣也實現了Serializable接口,支持序列化,也實現了Cloneable接口,能被克隆。
Hashtable繼承於Dictionary類,實現了Map接口。Dictionary是聲明瞭操做"鍵值對"函數接口的抽象類。 有一點注意,HashTable除了線程安全以外(實際上是直接在方法上增長了synchronized關鍵字,比較古老,落後,低效的同步方式),還有就是它的key、value都不爲null。另外Hashtable 也有 初始容量 和 加載因子。
public Hashtable() { this(11, 0.75f); }
默認加載因子也是 0.75,HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。由於HashTable是直接使用除留餘數法定位地址。且Hashtable計算hash值,直接用key的hashCode()。
還要注意:前面說了Hashtable中key和value都不容許爲null,而HashMap中key和value都容許爲null(key只能有一個爲null,而value則能夠有多個爲null)。但如在Hashtable中有相似put(null,null)的操做,編譯一樣能夠經過,由於key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。
最後針對擴容:Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。
HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。
理解HashMap是Hashtable的輕量級實現(非線程安全的實現,hashtable是非輕量級,線程安全的),都實現Map接口,主要區別在於:
一、因爲HashMap非線程安全,在只有一個線程訪問的狀況下,效率要高於HashTable
二、HashMap容許將null做爲一個entry的key或者value,而Hashtable不容許。
三、HashMap把Hashtable的contains方法去掉了,改爲containsValue和containsKey。由於contains方法容易讓人引發誤解。
四、Hashtable繼承自陳舊的Dictionary類,而HashMap是Java1.2引進的Map 的一個實現。
五、Hashtable和HashMap擴容的方法不同,HashTable中hash數組默認大小11,擴容方式是 old*2+1。HashMap中hash數組的默認大小是16,並且必定是2的指數,增長爲原來的2倍,沒有加1。
六、二者經過hash值散列到hash表的算法不同,HashTbale是古老的除留餘數法,直接使用hashcode,然後者是強制容量爲2的冪,從新根據hashcode計算hash值,在使用hash 位與 (hash表長度 – 1),也等價取膜,但更加高效,取得的位置更加分散,偶數,奇數保證了都會分散到。前者就不能保證。
七、另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。
第一,若是多個線程同時使用put方法添加元素
假設正好存在兩個put的key發生了碰撞(hash值同樣),那麼根據HashMap的實現,這兩個key會添加到數組的同一個位置,這樣最終就會發生其中一個線程的put的數據被覆蓋。
第二,若是多個線程同時檢測到元素個數超過數組大小*loadFactor
這樣會發生多個線程同時對hash數組進行擴容,都在從新計算元素位置以及複製數據,可是最終只有一個線程擴容後的數組會賦給table,也就是說其餘線程的都會丟失,而且各自線程put的數據也丟失。且會引發死循環的錯誤。
具體細節上的緣由,能夠參考:不正當使用HashMap致使cpu 100%的問題追究
一、直接使用Hashtable,可是當一個線程訪問HashTable的同步方法時,其餘線程若是也要訪問同步方法,會被阻塞住。舉個例子,當一個線程使用put方法時,另外一個線程不但不可使用put方法,連get方法都不能夠,效率很低,如今基本不會選擇它了。
二、HashMap能夠經過下面的語句進行同步:
Collections.synchronizeMap(hashMap);
三、直接使用JDK 5 以後的 ConcurrentHashMap,若是使用Java 5或以上的話,請使用ConcurrentHashMap。
直接分析源碼吧
1 // synchronizedMap方法 2 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { 3 return new SynchronizedMap<>(m); 4 } 5 // SynchronizedMap類 6 private static class SynchronizedMap<K,V> 7 implements Map<K,V>, Serializable { 8 private static final long serialVersionUID = 1978198479659022715L; 9 10 private final Map<K,V> m; // Backing Map 11 final Object mutex; // Object on which to synchronize 12 13 SynchronizedMap(Map<K,V> m) { 14 this.m = Objects.requireNonNull(m); 15 mutex = this; 16 } 17 18 SynchronizedMap(Map<K,V> m, Object mutex) { 19 this.m = m; 20 this.mutex = mutex; 21 } 22 23 public int size() { 24 synchronized (mutex) {return m.size();} 25 } 26 public boolean isEmpty() { 27 synchronized (mutex) {return m.isEmpty();} 28 } 29 public boolean containsKey(Object key) { 30 synchronized (mutex) {return m.containsKey(key);} 31 } 32 public boolean containsValue(Object value) { 33 synchronized (mutex) {return m.containsValue(value);} 34 } 35 public V get(Object key) { 36 synchronized (mutex) {return m.get(key);} 37 } 38 39 public V put(K key, V value) { 40 synchronized (mutex) {return m.put(key, value);} 41 } 42 public V remove(Object key) { 43 synchronized (mutex) {return m.remove(key);} 44 } 45 // 省略其餘方法 46 }
從源碼中看出 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操做是線程安全的,故效率其實也不高。
前面分析了,Hashtable 的擴容方法是乘2再+1,不是簡單的乘2,故hashtable保證了容量永遠是奇數,結合以前分析hashmap的重算hash值的邏輯,就明白了,由於在數據分佈在等差數據集合(如偶數)上時,若是公差與桶容量有公約數 n,則至少有(n-1)/n 數量的桶是利用不到的,故以前的hashmap 會在取模(使用位與運算代替)哈希前先作一次哈希運算,調整hash值。這裏hashtable比較古老,直接使用了除留餘數法,那麼就須要設置容量起碼不是偶數(除(近似)質數求餘的分散效果好)。而JDK開發者選了11。
參考更新的jdk 8對hashmap的的改進部分整理,而且還能引伸出高級數據結構——紅黑樹,這又能引出不少問題……學無止境啊!
臨時小結:感受針對Java的hashmap和hashtable面試,或者理解,到這裏就能夠了,具體就是多寫代碼實踐。