一.引言 java
HashMap應該算是Java後端工程師面試的必問題,由於其中的知識點太多,很適合用來考察面試者的Java基礎。面試
HashMap做爲使用頻率最高的用於映射(鍵值對)處理的數據類型。隨着JDK(Java Developmet Kit)版本的更新,JDK1.8對HashMap底層的實現進行了優化, 例如引入紅黑樹的數據結構和擴容的優化等。 結合JDK1.7和JDK1.8的區別讓咱們一塊兒來探討一下吧!算法
二.簡介數據庫
Hash法的概念後端
散列法(Hashing)是一種將字符組成的字符串轉換爲固定長度(通常是更短長度)的數值或索引值的方法,稱爲散列法,也叫哈希法。因爲經過更短的哈希值比用原始值進行數據庫搜索更快,這種方法通常用來在數據庫中創建索引並進行搜索,同時還用在各類解密算法中。數組
HashMap 和 HashTable安全
Hashtable 是早期Java類庫提供的一個哈希表實現, 不少映射的經常使用功能與HashMap相似,不一樣的是它承自Dictionary類,而且是線程安全的,任一時間只有一個線程能寫Hashtable ,不支持 null 鍵和null值,因爲同步致使的性能開銷,因此已經不多被推薦使用。數據結構
HashMap與 HashTable主要區別在於 HashMap 不是同步的,支持 null 鍵和null值等。一般狀況下,HashMap 進行 put 或者 get 操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選。HashMap非線程安全,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections的synchronizedMap方法使HashMap具備線程安全的能力,或者使用ConcurrentHashMap。多線程
三.HashMap的實現原理併發
HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
哈希衝突
咱們都知道判斷對象是否相等的時候可使用 hash值這種方式來,同時咱們還知道這種方式是存在缺點的,有可能兩個對象的內容不正確,可是兩個對象的hash值卻同樣,這是存在的.
那麼, 若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦? 也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。
數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?
咱們能夠經過如下幾種方式解決 :
針對哈希表直接定址可能存在hash衝突,舉一個簡單的例子,例如:
第一個鍵值對A進來,經過計算其key的hash獲得的index=0。記作:Entry[0] = A。
第二個鍵值對B,經過計算其index也等於0, HashMap會將B.next =A,Entry[0] =B,
第三個鍵值對C,經過計算其index也等於0,那麼C.next = B,Entry[0] = C;
這樣咱們發現index=0的地方事實上存取了A,B,C三個鍵值對,它們經過next這個屬性連接在一塊兒。 對於不一樣的元素,可能計算出了相同的函數值,這樣就產生了hash 衝突,那要解決衝突,又有哪些方法呢?具體以下:
a. 鏈地址法:將哈希表的每一個單元做爲鏈表的頭結點,全部哈希地址爲 i 的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。
b. 開放定址法:即發生衝突時,去尋找下一個空的哈希地址。只要哈希表足夠大,總能找到空的哈希地址。
c. 再哈希法:即發生衝突時,由其餘的函數再計算一次哈希值。
d. 創建公共溢出區:將哈希表分爲基本表和溢出表,發生衝突時,將衝突的元素放入溢出表。
而咱們的HashMap採用的是鏈地址法,即上面提到 數組 + 鏈表的方式. 當兩個對象的哈希值相同時,它們的哈希桶位置相同,碰撞就會發生。此時,能夠將 put 進來的 K- V 對象插入到鏈表的尾部。對於儲存在同一個哈希桶位置的鏈表對象,可經過鍵對象的equals()方法用來找到鍵值對。
四.HashMap的內部數據結構
HashMap內部維護的數據結構是數組+鏈表,每一個鍵值對都存儲在HashMap的靜態內部類Entry中,結構如圖 :
五 . HashMap插入數據原理圖
- 判斷數組是否爲空,爲空進行初始化;
- 不爲空,計算 k 的 hash 值,經過 (n-1) & hash 計算應當存放在數組中的下標 index;
- 查看 table[index] 是否存在數據,沒有數據就構造一個Node節點存放在 table[index] 中;
- 存在數據,說明發生了hash衝突(存在二個節點key的hash值同樣), 繼續判斷key是否相等,相等,用新的value替換原數據(onlyIfAbsent爲false);
- 若是不相等,判斷當前節點類型是否是樹型節點,若是是樹型節點,創造樹型節點插入紅黑樹中;(若是當前節點是樹型節點證實當前已是紅黑樹了)
- 若是不是樹型節點,建立普通Node加入鏈表中;判斷鏈表長度是否大於 8而且數組長度大於64, 大於的話鏈表轉換爲紅黑樹;
- 插入完成以後判斷當前節點數是否大於閾值,若是大於開始擴容爲原數組的二倍。
六.HashMap 是否線程安全?如何解決線程不安全的問題
HashMap是否線程安全?
不是,在多線程環境下,1.7 會產生死循環、數據丟失、數據覆蓋的問題,1.8 中會有數據覆蓋的問題,以1.8爲例,當A線程判斷index位置爲空後正好掛起,B線程開始往index位置的寫入節點數據,這時A線程恢復現場,執行賦值操做,就把A線程的數據給覆蓋了;還有++size這個地方也會形成多線程同時擴容等問題。
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() resize(); afterNodeInsertion(evict); return null; }
如何解決線程不安全的問題?
Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap能夠實現線程安全的Map。
HashTable是直接在操做方法上加synchronized關鍵字,鎖住整個數組,粒度比較大;
Collections.synchronizedMap是使用Collections集合工具的內部類,經過傳入Map封裝出一個SynchronizedMap對象,內部定義了一個對象鎖,方法內經過對象鎖實現;
ConcurrentHashMap使用分段鎖,下降了鎖粒度,讓併發度大大提升。
七.HashMap 在 JDk 1.8 和 JDK 1.7 中有什麼區別?
1.發生hash衝突時
JDK 1.7 :
發生hash衝突時,新元素插入到鏈表頭中,即新元素老是添加到數組中,就元素移動到鏈表中。
JDK 1.8 :
發生hash衝突後,會優先判斷該節點的數據結構式是紅黑樹仍是鏈表,若是是紅黑樹,則在紅黑樹中插入數據;若是是鏈表,則將數據插入到鏈表的尾部並判斷鏈表長度是否大於8,若是大於8要轉成紅黑樹。
2.新增節點時
JDK 1.7 :
使用了 頭插法
JDK 18. :
使用了 尾插法
3.擴容時
JDK 1.7 :
在擴容resize()過程當中,採用單鏈表的頭插入方式,在將舊數組上的數據 轉移到 新數組上時,轉移操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉移數據、擴容後,容易出現鏈表逆序的狀況 。 多線程下resize()容易出現死循環。此時若(多線程)併發執行 put()操做,一旦出現擴容狀況,則 容易出現 環形鏈表,從而在獲取數據、遍歷鏈表時 造成死循環(Infinite Loop),即 死鎖的狀態 。
JDK 1.8 :
因爲 JDK 1.8 轉移數據操做 = 按舊鏈表的正序遍歷鏈表、在新鏈表的尾部依次插入,因此不會出現鏈表 逆序、倒置的狀況,故不容易出現環形鏈表的狀況 ,但jdk1.8還是線程不安全的,由於沒有加同步鎖保護。
建議 :
1.使用的時候設置初始化值,避免屢次擴容的性能消耗
2.使用自定義對象做爲key時,須要重寫hashCode()和equals()方法
3.多線程環境儘可能使用ConcurrentHashMap來代替HashMap,HashTable也能夠,可是用的少
這就是小喵今天的分享了
知識有點亂,還請小夥伴們到時候縷縷思路哦!
(^_^)~喵~!!