讓咱們先從構造函數提及,HashMap有四個構造方法,別慌php
// 1.無參構造方法、 // 構造一個空的HashMap,初始容量爲16,負載因子爲0.75 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
無參構造方法就沒什麼好說的了。java
// 2.構造一個初始容量爲initialCapacity,負載因子爲0.75的空的HashMap, public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
HashMap(int initialCapacity)
這個構造方法調用了1.3中的構造方法。node
// 3.構造一個空的初始容量爲initialCapacity,負載因子爲loadFactor的HashMap public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //最大容量 //static final int MAXIMUM_CAPACITY = 1 << 30;
當指定的初始容量< 0時拋出IllegalArgumentException異常,當指定的初始容量> MAXIMUM_CAPACITY時,就讓初始容量 = MAXIMUM_CAPACITY。算法
當負載因子小於0或者不是數字時,拋出IllegalArgumentException異常。bootstrap
設定threshold。 這個threshold = capacity * load factor 。當HashMap的size到了threshold時,就要進行resize,也就是擴容。數組
tableSizeFor()的主要功能是返回一個比給定整數大且最接近的2的冪次方整數,如給定10,返回2的4次方16.安全
咱們進入tableSizeFor(int cap)的源碼中看看:數據結構
// Returns a power of two size for the given target capacity. static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
note: HashMap要求容量必須是2的冪。多線程
首先,int n = cap -1是爲了防止cap已是2的冪時,執行完後面的幾條無符號右移操做以後,返回的capacity是這個cap的2倍,由於cap已是2的冪了,就已經知足條件了。 若是不懂能夠往下看完幾個無符號移位後再回來看。(建議本身在紙上畫一下)併發
若是n這時爲0了(通過了cap-1以後),則通過後面的幾回無符號右移依然是0,最後返回的capacity是1(最後有個n+1的操做)。這裏只討論n不等於0的狀況
以16位爲例,假設開始時 n 爲 0000 1xxx xxxx xxxx (x表明不關心0仍是1)
第一次右移 n |= n >>> 1
因爲n不等於0,則n的二進制表示中總會有一bit爲1,這時考慮最高位的1。經過無符號右移1位,則將最高位的1右移了1位,再作或操做,使得n的二進制表示中與最高位的1緊鄰的右邊一位也爲1,如0000 11xx xxxx xxxx 。
第二次右移 n |= n >>> 2
注意,這個n已經通過了n |= n >>> 1; 操做。此時n爲0000 11xx xxxx xxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,而後再與原來的n作或操做,這樣n的二進制表示的高位中會有4個連續的1。如0000 1111 xxxx xxxx 。
第三次右移 n |= n >>> 4
此次把已經有的高位中的連續的4個1,右移4位,再作或操做,這樣n的二進制表示的高位中會有8個連續的1。如0000 1111 1111 xxxx 。
第。。。,你還忍心讓我繼續推麼?相信聰明的你已經想出來了,容量最大也就是32位的正數,因此最後一次 n |= n >>> 16; 能夠保證最高位後面的所有置爲1。固然若是是32個1的話,此時超出了MAXIMUM_CAPACITY ,因此取值到 MAXIMUM_CAPACITY 。
tableSizeFor示例圖
注意,獲得的這個capacity卻被賦值給了threshold。 這裏我和這篇博客的博主開始的想法同樣,認爲應該這麼寫:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
由於這樣子才符合threshold的定義:
threshold = capacity * load factor;
可是,請注意,在構造方法中,並無對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold從新計算 。
我說一下我在理解這個tableSizeFor函數中間遇到的坑吧,我在想若是n=-1時的狀況,由於初始容量能夠傳進來0。我將n= -1 和下面幾條運算一塊兒新寫了個測試程序,發現輸出都是 -1。
這是由於計算機中數字是由補碼存儲的,-1的補碼是 0xffffffff。因此無符號右移以後再進行或運算以後仍是 -1。
那我想若是就無符號右移呢? 好比-1>>>10。聽我娓娓道來,32個1無符號右移10位後,高10位爲0,低22位爲1,此時這個數變成了正數,因爲正數的補碼和原碼相同,因此就變成了0x3FFFFF即10進制的4194303。真刺激。
好開森,這個構造方法咱們算是拿下了。怎麼樣,我猜你如今必定很激動,Hey,old Fe,這纔剛開始。接下來看最後一個構造方法。
// 4. 構造一個和指定Map有相同mappings的HashMap,初始容量能充足的容下指定的Map,負載因子爲0.75 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
套路,直接看 putMapEntries(m,false) 。源碼以下:
/** * 將m的全部元素存入本HashMap實例中 */ final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { // 獲得 m 中元素的個數 int s = m.size(); // 當 m 中有元素時,則需將map中元素放入本HashMap實例。 if (s > 0) { // 判斷table是否已經初始化,若是未初始化,則先初始化一些變量。(table初始化是在put時) if (table == null) { // pre-size // 根據待插入的map 的 size 計算要建立的 HashMap 的容量。 float ft = ((float) s / loadFactor) + 1.0F; int t = ((ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIMUM_CAPACITY); // 把要建立的 HashMap 的容量存在 threshold 中 if (t > threshold) threshold = tableSizeFor(t); } // 若是table初始化過,由於別的函數也會調用它,因此有可能HashMap已經被初始化過了。 // 判斷待插入的 map 的 size,若 size 大於 threshold,則先進行 resize(),進行擴容 else if (s > threshold) resize(); // 而後就開始遍歷 帶插入的 map ,將每個 <Key ,Value> 插入到本HashMap實例。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); // put(K,V)也是調用 putVal 函數進行元素的插入 putVal(hash(key), key, value, false, evict); } } }
介紹putVal方法前,說一下HashMap的幾個重要的成員變量:
/** * The table, initialized on first use, and resized as necessary. When * allocated, length is always a power of two. (We also tolerate length zero in * some operations to allow bootstrapping mechanics that are currently not * needed.) */ // 實際存儲key,value的數組,只不過key,value被封裝成Node了 transient Node<K, V>[] table; /** * The number of key-value mappings contained in this map. */ transient int size; /** * The number of times this HashMap has been structurally modified Structural * modifications are those that change the number of mappings in the HashMap or * otherwise modify its internal structure (e.g., rehash). This field is used to * make iterators on Collection-views of the HashMap fail-fast. (See * ConcurrentModificationException). */ transient int modCount; /** * The next size value at which to resize (capacity * load factor). * * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) // 由於 tableSizeFor(int) 返回值給了threshold int threshold; /** * The load factor for the hash table. * * @serial */ final float loadFactor;
其實就是哈希表。HashMap使用鏈表法避免哈希衝突(相同hash值),當鏈表長度大於TREEIFY_THRESHOLD(默認爲8)時,將鏈表轉換爲紅黑樹,固然小於UNTREEIFY_THRESHOLD(默認爲6)時,又會轉回鏈表以達到性能均衡。 咱們看一張HashMap的數據結構(數組+鏈表+紅黑樹 )就更能理解table了:
HashMap的數據結構
回到putMapEntries函數中,若是table爲null,那麼這時就設置合適的threshold,若是不爲空而且指定的map的size>threshold,那麼就resize()。而後把指定的map的全部Key,Value,經過putVal添加到咱們建立的新的map中。
putVal中傳入了個hash(key),那咱們就先來看看hash(key):
/** * key 的 hash值的計算是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16) * 主要是從速度、功效、質量來考慮的,這麼作能夠在數組table的length比較小的時候 * 也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
異或運算:
(h = key.hashCode()) ^ (h >>> 16)
原 來 的 hashCode :
1111 1111 1111 1111 0100 1100 0000 1010
移位後的hashCode:
0000 0000 0000 0000 1111 1111 1111 1111
進行異或運算 結果:
1111 1111 1111 1111 1011 0011 1111 0101
這樣作的好處是,能夠將hashcode高位和低位的值進行混合作異或運算,並且混合後,低位的信息中加入了高位的信息,這樣高位的信息被變相的保留了下來。摻雜的元素多了,那麼生成的hash值的隨機性會增大。
剛纔咱們漏掉了resize()和putVal() 兩個函數,如今咱們按順序分析一波:
首先resize() ,先看一下哪些函數調用了resize(),從而在總體上有個概念:
調用了resize的函數.png
接下來上源碼:
final Node<K,V>[] resize() { // 保存當前table Node<K,V>[] oldTab = table; // 保存當前table的容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 保存當前閾值 int oldThr = threshold; // 初始化新的table容量和閾值 int newCap, newThr = 0; /* 1. resize()函數在size > threshold時被調用。oldCap大於 0 表明原來的 table 表非空, oldCap 爲原表的大小,oldThr(threshold) 爲 oldCap × load_factor */ if (oldCap > 0) { // 若舊table容量已超過最大容量,更新閾值爲Integer.MAX_VALUE(最大整形值),這樣之後就不會自動擴容了。 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 } /* 2. resize()函數在table爲空被調用。oldCap 小於等於 0 且 oldThr 大於0,表明用戶建立了一個 HashMap,可是使用的構造函數爲 HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity) 或 HashMap(Map<? extends K, ? extends V> m),致使 oldTab 爲 null,oldCap 爲0, oldThr 爲用戶指定的 HashMap的初始容量。 */ else if (oldThr > 0) // initial capacity was placed in threshold //當table沒初始化時,threshold持有初始容量。還記得threshold = tableSizeFor(t)麼; newCap = oldThr; /* 3. resize()函數在table爲空被調用。oldCap 小於等於 0 且 oldThr 等於0,用戶調用 HashMap()構造函數建立的 HashMap,全部值均採用默認值,oldTab(Table)表爲空,oldCap爲0,oldThr等於0, */ else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 新閾值爲0 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"}) // 初始化table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 把 oldTab 中的節點 reHash 到 newTab 中去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 若節點是單個節點,直接在 newTab 中進行重定位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操做 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 如果鏈表,進行鏈表的 rehash 操做 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 將同一桶中的元素根據(e.hash & oldCap)是否爲0進行分割(代碼後有圖解,能夠回過頭再來看),分紅兩個不一樣的鏈表,完成rehash do { next = e.next; // 根據算法 e.hash & oldCap 判斷節點位置rehash 後是否發生改變 //最高位==0,這是索引不變的鏈表。 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //最高位==1 (這是索引起生改變的鏈表) else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { // 原bucket位置的尾指針不爲空(即還有node) loTail.next = null; // 鏈表最後得有個null newTab[j] = loHead; // 鏈表頭指針放在新桶的相同下標(j)處 } if (hiTail != null) { hiTail.next = null; // rehash 後節點新的位置必定爲原來基礎上加上 oldCap,具體解釋看下圖 newTab[j + oldCap] = hiHead; } } } } } return newTab; } }
引自美團點評技術博客。咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。看下圖能夠明白這句話的意思,n爲table的長度,圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。
hashMap 1.8 哈希算法例圖1.png
元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:
hashMap 1.8 哈希算法例圖2.png
所以,咱們在擴充HashMap的時候,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖 :
jdk1.8 hashMap擴容例圖.png
何時擴容:經過HashMap源碼能夠看到是在put操做時,即向容器中添加元素時,判斷當前容器中元素的個數是否達到閾值(當前數組長度乘以加載因子的值)的時候,就要自動擴容了。
擴容(resize):其實就是從新計算容量;而這個擴容是計算出所需容器的大小以後從新定義一個新的容器,將原來容器中的元素放入其中。
resize()告一段落,接下來看 putVal() 。
上源碼:
//實現put和相關方法。 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是table爲空或者長度爲0,則resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //肯定插入table的位置,算法是(n - 1) & hash,在n爲2的冪時,至關於取摸操做。 ////找到key值對應的槽而且是第一個,直接加入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //在table的i位置發生碰撞,有兩種狀況,一、key值是同樣的,替換value值, //二、key值不同的有兩種處理方式:2.一、存儲在i位置的鏈表;2.二、存儲在紅黑樹中 else { Node<K,V> e; K k; //第一個node的hash值即爲要加入元素的hash if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //2.2 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //2.1 else { //不是TreeNode,即爲鏈表,遍歷鏈表 for (int binCount = 0; ; ++binCount) { ///鏈表的尾端也沒有找到key值相同的節點,則生成一個新的Node, //而且判斷鏈表的節點個數是否是到達轉換成紅黑樹的上界達到,則轉換成紅黑樹。 if ((e = p.next) == null) { // 建立鏈表節點並插入尾部 p.next = newNode(hash, key, value, null); ////超過了鏈表的設置長度8就轉換成紅黑樹 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; } } //若是e不爲空就替換舊的oldValue值 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; }
注:hash 衝突發生的幾種狀況:
1.兩節點key 值相同(hash值必定相同),致使衝突;
2.兩節點key 值不一樣,因爲 hash 函數的侷限性致使hash 值相同,衝突;
3.兩節點key 值不一樣,hash 值不一樣,但 hash 值對數組長度取模後相同,衝突;
相比put方法,get方法就比較簡單,這裏就不說了。
一、JDK1.7用的是頭插法,而JDK1.8及以後使用的都是尾插法,那麼爲何要這樣作呢?由於JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法就是可以提升插入的效率,可是也會容易出現逆序且環形鏈表死循環問題。可是在JDK1.8以後是由於加入了紅黑樹使用尾插法,可以避免出現逆序且鏈表死循環的問題。
二、擴容後數據存儲位置的計算方式也不同:
在JDK1.7的時候是直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1) 。
而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。
三、JDK1.7的時候使用的是數組+ 單鏈表的數據結構。可是在JDK1.8及以後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(N)變成O(logN)提升了效率)。
HashMap 在併發時可能出現的問題主要是兩方面:
put的時候致使的多線程數據不一致
好比有兩個線程A和B,首先A但願插入一個key-value對到HashMap中,首先計算記錄所要落到的 hash桶的索引座標,而後獲取到該桶裏面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A同樣執行,只不過線程B成功將記錄插到了桶裏面,假設線程A插入的記錄計算出來的 hash桶索引和線程B要插入的記錄計算出來的 hash桶索引是同樣的,
那麼當線程B成功插入以後,線程A再次被調度運行時,它依然持有過時的鏈表頭可是它對此一無所知,以致於它認爲它應該這樣作,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,形成了數據不一致的行爲。
resize而引發死循環
這種狀況發生在HashMap自動擴容時,當2個線程同時檢測到元素個數超過 數組大小 × 負載因子。
此時2個線程會在put()方法中調用了resize(),兩個線程同時修改一個鏈表結構會產生一個循環鏈表(JDK1.7中,會出現resize先後元素順序倒置的狀況)。接下來再想經過get()獲取某一個元素,就會出現死循環。
HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。
HashMap幾乎能夠等價於Hashtable,除了HashMap是非synchronized的,並能夠接受null(HashMap能夠接受爲null的鍵值(key)和值(value),而Hashtable則不行)。
HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程能夠共享一個Hashtable;而若是沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。
另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。
因爲Hashtable是線程安全的也是synchronized,因此在單線程環境下它比HashMap要慢。若是你不須要同步,只須要單一線程,那麼使用HashMap性能要好過Hashtable。
HashMap不能保證隨着時間的推移Map中的元素次序是不變的。
須要注意的重要術語:
sychronized意味着在一次僅有一個線程可以更改Hashtable。就是說任何線程要更新Hashtable時要首先得到同步鎖,其它線程要等到同步鎖被釋放以後才能再次得到同步鎖更新Hashtable。
Fail-safe和iterator迭代器相關。若是某個集合對象建立了Iterator或者ListIterator,而後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程能夠經過set()方法更改集合對象是容許的,由於這並無從「結構上」更改集合。可是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。
結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。
HashMap能夠經過下面的語句進行同步:
Map m = Collections.synchronizeMap(hashMap);