不多說,直接上干貨!
福利 => 每天都推送
HashMap的實現原理
HashMap是基於java.util.map接口的實現,該實現提供了所有的對Map的可選操作,同時也允許null類型的key以及value (HashTable與此大致相同,只是HashTable是同步的,不過HashTable一般被認為是已經過時的,很少有人再去用了)。
HashMap不保證Map中的順序,特別是不能保證數據在一段時間內的順序性。
如果散列函數(Hash函數)在正確的在桶中分散元素,HashMap的實現提供了對基本的操作(put與get)時間性能為常數。
集合視圖的迭代需要的時間與HashMap實例的容量capacity值(桶數)和大小(鍵值對個數)成正比,所以如果對迭代器的性能比較關注, 那么不要把初始化的容量capacity值設的太高或不要將裝填因子load factor設的太小就非常重要。
HashMap的實例總有兩個非常重要的影響性能的參數:初始容量大小(initial capacity)與裝填因子(load factor)。
在HashMap中,capacity就是buckets的數量,初始容量大小就是在HashMap創建的時候指定的capacity。
裝填因子指的是HashMap在多滿的時候就需要提前自動擴容了(如初始化容量16,裝填因子0.75,一旦元素個數 超過16*0.75=12個的時候,就需要對容量進行擴充,翻倍) 擴容需要rehash,也就是說HashMap內部結構會被重新構建。
通常來講,默認的裝填因子0.75提供了一個基於時間與空間代價比較好的平衡。桶中裝的更滿意味着空間開銷更低, 但是查找開銷變大(反映到HashMap中就是包含get與put在內各種操作開銷變大,時間變長)。
當設置初始容量大小時,應該考慮HashMap中預期的數量以及裝填因子,這樣才能讓rehash執行的次數最少, 如果初始化大小的值比實際數據的條數乘以裝填因子的積還要大,那么就不會發生rehash。
如果有非常多的記錄存在HashMap中,那么在初始化這個HashMap的時候將容量capacity設置的足夠大,比自動rehash 擴容效率更高。
注意如果很多key有了相同的hashCode值,那么會對性能造成很大的影響(產生聚集問題,rehash次數變多,性能下降),為了改善這種情況 當key可以進行比較(繼承了Comparable接口之類的),HashMap會采用對key進行比較排序。
注意:HashMap不是線程同步的。
如果多線程並發訪問一個HashMap,同時至少有一個線程修改了HashMap的結構,那么該HashMap必須要在外部進行同步 (結構的改變指的是添加或刪除一個映射關系-鍵值對,只是根據key修改了value的值不會對HashMap造成結構上的影響) 這種同步操作通常在封裝有HashMap的Object中實現;如果沒有這樣的對象實現,那么HashMap需要采用Collections.synchronizedMap 來保證多線程並發環境下的數據同步問題,這個操作最好在創建HashMap的時候就去做。 Map m = Collections.synchronizedMap(new HashMap(…));
如果HashMap產生的迭代器創建后,HashMap數據結構又發生了變化,則除了remove方法外,迭代器iterator會拋出一個 ConcurrentModificationException異常,這就是迭代器的快速失敗(fail-fast)。 因此面對並發修改,迭代器采用快速而干凈的失敗來取代在未來的某一個不確定的時間產生不確定的后果。
注意:迭代器的快速失敗行為是無法保證的,通常來講,在非同步並發修改的情況下這種硬性保證都沒法給出,快速失敗也只能盡量拋出 ConcurrentModificationException異常,所以不能寫一段代碼去基於該異常做出義務邏輯處理,最好快速失敗進行BUG探測。
更多詳細,見
牛客網Java刷題知識點之Java 集合框架的構成、集合框架中的迭代器Iterator、集合框架中的集合接口Collection(List和Set)、集合框架中的Map集合
牛客網Java刷題知識點之Map的兩種取值方式keySet和entrySet、HashMap 、Hashtable、TreeMap、LinkedHashMap、ConcurrentHashMap 、WeakHashMap
HashMap的存儲結構
HashMap的存儲,本質上是構造了一個table,根據計算的hashCode將對應的KV存儲到該table中,一旦發生hashCode沖突,那么就會將該KV放到對應的已有元素的后面, 此時,形成了一個鏈表式的存儲結構,即:HashMap 就是一個table數組 + 鏈表實現的存儲結構(在JDK1.8之前的存儲結構),如下圖所示:
從上圖中,我們可以發現哈希表是由數組 + 鏈表組成的,一個長度為16的數組中,每個元素存儲的是一個鏈表的頭結點。那么這些元素是按照什么樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。
牛客網Java刷題知識點之數組、鏈表、哈希表、 紅黑二叉樹
當構造出一個HashMap后,每次put一個元素時,會執行addEntry操作,addEntry中會執行創建一個Entry實體存儲當前的KV,如果有Hash沖突, 那么就會將當前的entry指向最后一個沖突的entry,同時將當前的entry放到鏈表中,執行完當前操作后,判斷是否需要resize,如果需要,則構造出一個新的entry數組(比之前大一倍), 並將原有的數組中的entry鏈表(或entry)拷貝到新的數組中。
在JDK1.8中有了一些變化,當鏈表的存儲的數據個數大於等於8的時候,不再采用鏈表存儲,而采用了紅黑樹存儲結構。如下圖所示:
這么做主要是查詢的時間復雜度上,鏈表為O(n),而紅黑樹一直是O(logn),一般來講,沖突(即為相同的hash值存儲的元素個數) 超過8個,紅黑樹的性能高於鏈表的查找性能。
HashMap在JDK1.6中的實現方式
put方法:
put方法完成數據的基本寫入操作,同時會驗證數據的有效性,如key是否為空的處理,計算hash值,hash值 的查找,並根據hash值找出所有的數據,判斷是否重復,如果重復,則替換,並返回舊值,如果不重復,則寫入數據(addEntry)。
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
對應的addEntry方法: addEntry方法實現了數據的寫入以及判斷是否需要對HashMap進行擴容。
/** * Adds a new entry with the specified key, value and hash code to * the specified bucket. It is the responsibility of this * method to resize the table if appropriate. * * Subclass overrides this to alter the behavior of put method. */ void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
resize方法: resize方法主要是對Map進行擴容的實際操作:構造存儲結構,並調用數據轉移方法transfer進行數據轉移
/** * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). */ void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
transfer 方法: 完成數據轉移,注意,轉移的過程就是兩個數組進行數據拷貝的過程,數據產生了倒置,新元素其實是插入到了第一個位置。
/** * Transfers all entries from current table to newTable. */ void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do {//每次循環都是將第一個元素指向了最新的數據,其他數據后移(鏈式存儲,更改指向) Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
Entry的構造: 鏈式存儲結構。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ··· }
HashMap在JDK1.7中的實現
JDK1.7中的實現與JDK1.6中的實現總體來說,基本沒有改進,區別不大。
put 方法:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
resize方法:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
transfer方法:
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } }
createEntry方法:
/** * Like addEntry except that this version is used when creating entries * as part of Map construction or "pseudo-construction" (cloning, * deserialization). This version needn't worry about resizing the table. * * Subclass overrides this to alter the behavior of HashMap(Map), * clone, and readObject. */ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
HashMap在JDK1.8中的實現
put方法: put方法中與JDK1.6/1.7相比,新增了判斷是否是TreeNode的邏輯,TreeNode即為紅黑樹, 同時添加了沖突的元素個數是否大於等於7的判斷(因為第一個是-1,所以還是8個元素),如果沖突的元素個數超過8個,則構造紅黑樹(treeifyBin方法),否則還是按照鏈表方式存儲。
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null)//取模運算,尋址無沖突,寫入數據 tab[i] = newNode(hash, key, value, null); else {//存在沖突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)//集合中已經是紅黑樹了 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) {//沖突時的數據寫入邏輯,判斷是否需要構造紅黑樹 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
treeifyBin方法: treeifyBin方法主要是將鏈式存儲的沖突的數據轉換成樹狀存儲結構
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
resize方法: 注意:JDK1.8中resize方法擴容時對鏈表保持了原有的順序。
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
HashMap的不足以及產生原因
多線程並發問題
在多線程並發環境下會有如下一種情況:
兩個線程同時操作時同時遇到HashMap需要擴容,且映射到相同的地址(key計算得到的HashCode相同),此時在擴容時可能發生一種情況, 兩個線程同時對HashMap進行擴容,擴容時做第一次循環時一個線程阻塞,另一個完成擴容,前一個繼續,那么就可能發生鏈表數據的相互指向問題, 造成get數據時遍歷的死循環
測試代碼如下:
final Map<Integer, String> map = new HashMap<>(2); map.put(3, "3"); Thread t = new Thread(new Runnable() { @Override public void run() { map.put(5, "5"); } }, "thread"); Thread t2 = new Thread(new Runnable() { @Override public void run() { map.put(7, "7"); } }, "thread2"); t.start(); t2.start(); t.join(); t2.join();
注意:我參考了大量的網上的一些博客,他們基本都是基於JDK1.6進行的分析,即:先寫入數據,再擴容:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//先寫數據 if (size++ >= threshold) resize(2 * table.length);//再擴容 }
由此,很容易產生上述問題,而在JDK1.7中發生了變化,先擴容,再進行數據寫入:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//先擴容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex);//再寫入數據 }
此時,先擴容,再寫入數據,就不會產生這種問題,但是會產生另一中問題,即: HashMap中有A元素,thread1 寫入B元素(碰撞),thread2寫入C元素(碰撞) 線程thread1先擴容,在寫入數據,同時線程thread2也進行該操作那么,最終互不影響, thread1得到:B->A鏈表,thread2得到:C->A鏈表,那么,在thread1中取數據,能否取到C元素? 多線程並發問題依舊存在。
在JDK1.8中,HashMap的這塊兒做了大量改進,死循環問題已經解決了:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //...... for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null);//直接追加到最后節點中 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//構造紅黑樹 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } //...... }
但是JDK1.8 中通過測試發現依然存在JDK1.7中的數據丟失情況。
同時在並發情況下會出現如下新的問題: Hash碰撞情況下:前一個線程判斷是鏈表存儲,准備寫入數據,但是寫入時阻塞,其他線程也准備寫入,發現數據鏈表為8,此時構造紅黑樹,然后完成數據轉移 前一個線程此后寫入數據時,就會出現類型錯誤:
Exception in thread "24590 _subThread" java.lang.ClassCastException: java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNode at java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1827) at java.util.HashMap$TreeNode.treeify(HashMap.java:1944) at java.util.HashMap$TreeNode.split(HashMap.java:2170) at java.util.HashMap.resize(HashMap.java:713) at java.util.HashMap.putVal(HashMap.java:662) at java.util.HashMap.put(HashMap.java:611) at com.lin.map.HashMapDeadLoopTest$1$1.run(HashMapDeadLoopTest.java:37) at java.lang.Thread.run(Thread.java:745)
相關測試代碼(可能需要多執行幾次):
public class HashMapDeadLoopTest { @Test public void test() throws InterruptedException { // final Map<String, String> map = Collections.synchronizedMap(new HashMap<>(2048)); final Map<String, String> map = new HashMap<>(2048); Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { final int x = i; new Thread(new Runnable() { @Override public void run() { put(map, x); } },i+" _subThread").start(); if(i!=0 && i%10000==0){ System.out.println("1:"+map.size()); } } } },"thread"); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { final int x = i; new Thread(new Runnable() { @Override public void run() { put(map, x); } },i+" _subThread2").start(); if(i!=0 && i%10000==0){ System.out.println("2:"+map.size()); } } } },"thread2"); Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { final int x = i; new Thread(new Runnable() { @Override public void run() { put(map, x); } },i+" _subThread3").start(); if(i!=0 && i%10000==0){ System.out.println("3:"+map.size()); } } } },"thread3"); t.start(); t2.start(); t3.start(); t.join(); t2.join(); t3.join(); } String getKey(){ return RandomStringUtils.randomAlphanumeric(32); } void put(Map<String,String> map,int index){ String key = getKey(); map.put(key, key); if(map.get(key)==null){ System.out.println(key+",元素缺失,index:"+index); } } }
總結
- HashMap的JDK1.6、JDK1.7、JDK1.8中的實現各不相同,尤其以JDK1.8變化最大,注意區分
JDK1.7 HashMap源碼分析
http://blog.csdn.net/qq_19431333/article/details/61614414
HashMap源碼詳解(JDK7版本)
JDK1.8 HashMap源碼分析
http://blog.csdn.net/qq_19431333/article/details/55505675
同時,大家可以關注我的個人博客:
http://www.cnblogs.com/zlslch/ 和 http://www.cnblogs.com/lchzls/ http://www.cnblogs.com/sunnyDream/
詳情請見:http://www.cnblogs.com/zlslch/p/7473861.html
人生苦短,我願分享。本公眾號將秉持活到老學到老學習無休止的交流分享開源精神,匯聚於互聯網和個人學習工作的精華干貨知識,一切來於互聯網,反饋回互聯網。
目前研究領域:大數據、機器學習、深度學習、人工智能、數據挖掘、數據分析。 語言涉及:Java、Scala、Python、Shell、Linux等 。同時還涉及平常所使用的手機、電腦和互聯網上的使用技巧、問題和實用軟件。 只要你一直關注和呆在群里,每天必須有收獲
對應本平台的討論和答疑QQ群:大數據和人工智能躺過的坑(總群)(161156071)
打開百度App,掃碼,精彩文章每天更新!歡迎關注我的百家號: 九月哥快訊