HashMap vs HashTablehtml
HashTable若是插入key/value爲null的值時,會報錯,可是hashmap不會,在hashmap中,null是做爲第0個元素的,至關因而作了特殊化處理。算法
前者是非線程安全的,後者是線程安全的. 後者線程安全的緣由就是由於後者的每個方法上都有一個synchronized,這樣雖然保障了線程安全,可是每次都要鎖整個class對象,而且還會阻止其餘synchronize方法的訪問,因此效率低下! HashTable已經廢棄了,儘可能別使用了.shell
因此綜上所述,HashTable效率低的緣由是由於全部訪問它的線程都必須競爭同一把鎖,若是容器裏面不一樣的數據有多把鎖,那麼執行的效率就高了,因此ConcurrentHashMap使用的就是鎖分段的技術.數據庫
JDK1.8以後,HashMap和以前1.7不一樣的是,再也不單純的由數組+鏈表的方式實現的(所謂的鏈地址法). 1.7因爲在Hash衝突的時候,在桶上造成的鏈表會愈來愈長,這樣在查詢的時候效率就會變低,1.8以後改爲了由紅黑樹實現.即:數組
因此,JAVA8中的HashMap是由: 數組+鏈表/紅黑樹組成安全
Java7使用Entry來表示每一個HashMap中的數據節點,8中使用了Node,基本沒什麼區別,都是key,value,hash和next這四個屬性來修飾鏈表,而紅黑樹的狀況須要使用TreeNode函數
默認容量爲: 16性能
這裏的默認容量爲何是2的n次冪?.net
查看它的put方法可知,key在Node[]中的下標爲: (n - 1) & hash。若是這個n是2的N次冪,那麼hash至關於和111***111作與運算,數據分散的就比較均勻。若是n不是在的N次冪,即hash有可能和1110作與運算,那麼最後一位怎麼與都是0,那至關於結尾是1的那幾個下標永遠都不會放數據了,好比0001,0011…這確定會增長碰撞的概率.線程
若是size > capacity*loadFactor的話,hashmap還會進行resize操做,會至關耗性能!因此若是事先能夠肯定你要放進hashMap中的數據大小。那麼應該儘可能設置成loadFactor * 2^n > initCapacity ,這樣既考慮了&的問題,也避免了resize的問題.
可是後面提到會有tableSizeFor()和在put的時候考慮loadFact來保證上面這兩個要求的.因此在初始化的時候,設置成本身知道的大小便可,衝突這些由hashmap自身來幫你減免!
不對,仍是得本身算下,若是你輸入的initcapacity爲7,那麼算出來的最近的2^n爲8,若是選擇的是默認的0.75,即最多放入6個元素就要擴容,你放到第7個的時候,仍是要resize()…
最大的capacity爲 2^30
默認負載因子爲0,75,即size > capacity * 0.75,hashmap就要進行擴容
特殊狀況下:
1)內存空間不少,時間效率要求很高,能夠下降loadFactor(儘可能讓table擴寬)
2)若是內存緊張,時間效率要求不高,能夠增長loadFactor
一個桶中,bin(箱子)的存儲方式由鏈表轉成紅黑樹的閾值爲8
一個桶中,由紅黑樹轉成鏈表的閾值,resize的時候可能會用到這個值.
當桶中的bin被樹化時最小的hash表容量.若是樹化時bin的數量太多會進行resize擴容.註釋中說MIN_TREEIFY_CAPACITY至少是 4 * TREEIFY_THRESHOLD
上面說了這麼多的bin,這裏該介紹下bin究竟是個什麼結構了.
每一個bin在HashMap表明存儲了一個K/V鍵值對,結構定義以下:
Hashmap中計算key的hash值
能夠看到它並無直接使用Object中生成hashcode的方法,這個方法叫擾動函數,和以前的要將capacity設計成2^n同樣,也是爲了減小碰撞用的.
根據前面可知,key在Node[]中的下標爲 key.hashCode & (n-1),咱們知道key.hashCode是一個很長的int類型的數字(範圍大概40億),而n-1顯然沒有這麼長,若是直相與,那麼只有key.hashCode的後面幾位參與運算了,顯然會使得碰撞很激烈!加了這個函數以後,讓高位也想辦法參與到運算中來,這樣就有可能進一步下降碰撞的可能性了!
用於存儲Node(K/V)對的hash表(數組),爲何是transient?
爲了解答這個問題,咱們須要明確下面事實:
Object.hashCode方法對於一個類的兩個實例返回的是不一樣的哈希值
能夠試想下面的場景:
咱們在機器A上算出對象A的哈希值與索引,而後把它插入到HashMap中,而後把該HashMap序列化後,在機器B上從新算對象的哈希值與索引,這與機器A上算出的是不同的,因此咱們在機器B上get對象A時,會獲得錯誤的結果。
因此說,當序列化一個HashMap對象時,保存Entry的table是不須要序列化進來的,由於它在另外一臺機器上是錯誤的,因此屬性這裏爲transient。
由於這個緣由,HashMap重寫了writeObject與readObject 方法
保存K/V對的Set
目前hashMap中K/V對的數量
每次對這個hashmap作操做,這個modCount就會改變.(CAS?!)
Threadhold表示當容量達到該值時,會進行resize
loadFactor表示用戶設置的負載因子大小
構造函數:
有三種,一種是無參的,這個沒啥好看的,一種是隻配置了initialCapacity,最後一種是設置了initcapacity和loadFactor。看第三種就夠了,第二種不過是將loadFactor這個形參用默認0.75傳入而已.
方法內部是將loadFactor這個屬性設置爲用戶輸入的大小,有意思的是tableSizeFor(initCap)這個函數,也就是說你輸入一個10,hash表的大小不必定就是10. 這個函數的功能就是用來保證容量應該大於cap,且爲2的整數冪.
隨便寫個數帶進去算一下就可知道,該算法的做用讓最高位的1後面的位全變爲1.而後再+1,獲得的就是恰巧的2的n次冪.
注意table的初始化是在第一次put的時候作的,那個時候還有考慮loadFactor再作一次tableSize的計算,那個時候獲得的就是最合適的那個2的n次冪的那個數了!厲害啊!
因此,看下put流程吧!這個很重要:
方法流程以下:
方法定義以下:
False表示,會改變existing value,至關因而key相同的話會作替換.
True表示,table不在creation mode.(這是啥意思?!)
第一次或者擴容的時候會調用resize():
那麼newCap=16, threshold = 0.75 * 16
Hash表大小爲: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]
注意,此時的threadhold(=initcapacity接近的2^n那個值) 和 loadFactor已經有值了,可是table==null,由於沒有初始化嘛,因此此時oldCap=0
此時 newCap = threadhold
threshold =newThreadHold = newCap * loadFactor (threadhold還變小了!…)
Hash表大小爲: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]
此時 oldTab !=null oldCap = oldTable.length oldThreadHold = threadhold
那麼 newCap = oldCap << 1 爲原來的一倍 newThreadholder也爲oldThr的一倍,即都擴大爲原先的一倍!
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
還要將原先舊Table裏的數據轉移到新的table中.
即 ++size > threshold 那麼須要進行resize
這裏讓我想到有問題的可能就是這個了,首次初始化,cap爲16,輸入的initcapacity爲15的時候,會涉及到一次擴容(若是沒有衝突的話—因此這裏的擴容一倍啥的操做應該是取了個折中!否則爲了避免衝突再擴容一倍很耗費空間啊!並且再擴容一倍也不能保證不衝突)
resize()流程分析:
(和Java7不一樣,8中resize後元素順序是不變的)
歸納起來就是:
若是已是MAXIMUM_CAPACITY,那麼返回原先數組,不然將容量擴大爲原來的一倍,即newCap = oldCap << 1, newThr=oldThr << 1
newCap = oldThr, newThr隨後會被指定爲newCap*loadFactor
按照默認的值初始化,即initCapacity=16, newThr=0.75*16
遷移數據的流程以下:
舉例:
好比原來容量是16,那麼就至關於index=e.hash & 0x1111
如今容量擴大了一倍,就是32,那麼index=e.hash & 0x11111
如今(e.hash & oldCap) == 0就代表:
已知: e.hash & 0x1111 = index
而且: e.hash & 0x10000 = 0
那麼: e.hash & 0x11111 不也就是原先index的值!
get()流程分析:
若是獲取到的Node爲null則返回null,不然返回Node中存儲的值。
點到getNode()中繼續查看
分四步進行:
首先,若是table未空,直接返回null。不然就查找節點
再者,計算hash獲得的位置剛符合,那直接返回。(只有一個bin的狀況)
若是是紅黑樹,按紅黑樹的方式查找
若是是鏈表,逐個查找。 找不到返回null。
entrySet分析:
遍歷的話,使用此種方式。比每次從Map中從新獲取一個key要快多了!
不是每次都是new EntrySet(),可是暫時沒找到這個東西在哪裏填充的。
網上的說法是遍歷的原理就是hashmap實現的原理。entrySet()該方法返回的是map包含的映射集合視圖,視圖的概念至關於數據庫中視圖。提供一個窗口,沒有具體到相關數據,而真正獲取數據仍是從table[]中來。(ps:能夠借鑑下hashmap的foreach方法,即先遍歷完數組中的第一個鏈表,再遍歷數組中的下一個鏈表…)
HashMap爲何線程不安全:
Java8之前線程不安全是在於在resize()的時候會在get的時候產生死循環,而之因此產生死循環是由於resize以後,元素的前後順序會相反。
即轉移的時候是這樣的:每次取出舊數組的頭結點的next,以後從新計算頭結點在新的Hash中的位置,而後將頭節點的next指向新的table[i],而後把table[i]設置成當前的頭結點,那麼就完成了頭結點的轉移。
這時候,線程一種3.next是7,線程二中7.next是3,e.next = newTable[i]就會造成了環形鏈表,因此在get的時候就會一直循環在這裏。
而且在迭代的過程當中,若是有線程修改了map,會拋出ConcurrentModificationException錯誤,就是所謂的fail-fast策略。
Java8以後,由於順序是相同的,因此上面的那個環形鏈表問題就沒有了。可是後面那個問題仍是有的,因此仍是線程不安全的。另外還有++size的操做也不是線程安全的!
參考:
http://www.iteye.com/topic/539465 (initailCapacity爲何要設置成2的n次冪?)
https://blog.csdn.net/dog250/article/details/46665743#comments (紅黑樹的一種解釋)