HashMap、CurrentHashMap 的實現原理基本都是BAT面試必考內容,阿里P8架構師談:深刻探討HashMap的底層結構、原理、擴容機制深刻談過hashmap的實現原理以及在JDK 1.8的實現區別,今天主要談CurrentHashMap的實現原理,以及在JDK1.7和1.8的區別。html
內容目錄:java
1.哈希表面試
2.ConcurrentHashMap與HashMap、HashTable的區別redis
3.CurrentHashMap在JDK1.7和JDK1.8版本的區別算法
1.介紹編程
哈希表就是一種以 鍵-值(key-indexed) 存儲數據的結構,咱們只要輸入待查找的值即key,便可查找到其對應的值。數組
哈希的思路很簡單,若是全部的鍵都是整數,那麼就可使用一個簡單的無序數組來實現:將鍵做爲索引,值即爲其對應的值,這樣就能夠快速訪問任意鍵的值。這是對於簡單的鍵的狀況,咱們將其擴展到能夠處理更加複雜的類型的鍵。緩存
2.鏈式哈希表安全
鏈式哈希表從根本上說是由一組鏈表構成。每一個鏈表均可以看作是一個「桶」,咱們將全部的元素經過散列的方式放到具體的不一樣的桶中。插入元素時,首先將其鍵傳入一個哈希函數(該過程稱爲哈希鍵),函數經過散列的方式告知元素屬於哪一個「桶」,而後在相應的鏈表頭插入元素。查找或刪除元素時,用同們的方式先找到元素的「桶」,而後遍歷相應的鏈表,直到發現咱們想要的元素。由於每一個「桶」都是一個鏈表,因此鏈式哈希表並不限制包含元素的個數。然而,若是表變得太大,它的性能將會下降。數據結構
3.應用場景
咱們熟知的緩存技術(好比redis、memcached)的核心其實就是在內存中維護一張巨大的哈希表,還有你們熟知的HashMap、CurrentHashMap等的應用。
1.HashMap
咱們知道HashMap是線程不安全的,在多線程環境下,使用Hashmap進行put操做會引發死循環,致使CPU利用率接近100%,因此在併發狀況下不能使用HashMap。
2.HashTable
HashTable和HashMap的實現原理幾乎同樣,差異無非是
可是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put全部相關操做都是synchronized的,這至關於給整個哈希表加了一把大鎖。
多線程訪問時候,只要有一個線程訪問或操做該對象,那其餘線程只能阻塞,至關於將全部的操做串行化,在競爭激烈的併發場景中性能就會很是差。
3.ConcurrentHashMap
主要就是爲了應對hashmap在併發環境下不安全而誕生的,ConcurrentHashMap的設計與實現很是精巧,大量的利用了volatile,final,CAS等lock-free技術來減小鎖競爭對於性能的影響。
咱們都知道Map通常都是數組+鏈表結構(JDK1.8該爲數組+紅黑樹)。
ConcurrentHashMap避免了對全局加鎖改爲了局部加鎖操做,這樣就極大地提升了併發環境下的操做速度,因爲ConcurrentHashMap在JDK1.7和1.8中的實現很是不一樣,接下來咱們談談JDK在1.7和1.8中的區別。
在JDK1.7中ConcurrentHashMap採用了數組+Segment+分段鎖的方式實現。
1.Segment(分段鎖)
ConcurrentHashMap中的分段鎖稱爲Segment,它即相似於HashMap的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表,同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
2.內部結構
ConcurrentHashMap使用分段鎖技術,將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問,可以實現真正的併發訪問。以下圖是ConcurrentHashMap的內部結構圖:
從上面的結構咱們能夠了解到,ConcurrentHashMap定位一個元素的過程須要進行兩次Hash操做。
第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部。
3.該結構的優劣勢
壞處
這一種結構的帶來的反作用是Hash的過程要比普通的HashMap要長
好處
寫操做的時候能夠只對元素所在的Segment進行加鎖便可,不會影響到其餘的Segment,這樣,在最理想的狀況下,ConcurrentHashMap能夠最高同時支持Segment數量大小的寫操做(恰好這些寫操做都很是平均地分佈在全部的Segment上)。
因此,經過這一種結構,ConcurrentHashMap的併發能力能夠大大的提升。
JDK8中ConcurrentHashMap參考了JDK8 HashMap的實現,採用了數組+鏈表+紅黑樹的實現方式來設計,內部大量採用CAS操做,這裏我簡要介紹下CAS。
CAS是compare and swap的縮寫,即咱們所說的比較交換。cas是一種基於鎖的操做,並且是樂觀鎖。在java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,好比經過給記錄加version來獲取數據,性能較悲觀鎖有很大的提升。
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和A的值是同樣的,那麼就將內存裏面的值更新成B。CAS是經過無限循環來獲取數據的,若果在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程須要自旋,到下次循環纔有可能機會執行。
JDK8中完全放棄了Segment轉而採用的是Node,其設計思想也再也不是JDK1.7中的分段鎖思想。
Node:保存key,value及key的hash值的數據結構。其中value和next都用volatile修飾,保證併發的可見性。
<strong>class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; //... 省略部分代碼 } </strong>
Java8 ConcurrentHashMap結構基本上和Java8的HashMap同樣,不過保證線程安全性。
在JDK8中ConcurrentHashMap的結構,因爲引入了紅黑樹,使得ConcurrentHashMap的實現很是複雜,咱們都知道,紅黑樹是一種性能很是好的二叉查找樹,其查找性能爲O(logN),可是其實現過程也很是複雜,並且可讀性也很是差,Doug
Lea的思惟能力確實不是通常人能比的,早期徹底採用鏈表結構時Map的查找時間複雜度爲O(N),JDK8中ConcurrentHashMap在鏈表的長度大於某個閾值的時候會將鏈表轉換成紅黑樹進一步提升其查找性能。
其實能夠看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增長了同步的操做來控制併發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹。
1.數據結構:取消了Segment分段鎖的數據結構,取而代之的是數組+鏈表+紅黑樹的結構。2.保證線程安全機制:JDK1.7採用segment的分段鎖機制實現線程安全,其中segment繼承自ReentrantLock。JDK1.8採用CAS+Synchronized保證線程安全。3.鎖的粒度:原來是對須要進行數據操做的Segment加鎖,現調整爲對每一個數組元素加鎖(Node)。4.鏈表轉化爲紅黑樹:定位結點的hash算法簡化會帶來弊端,Hash衝突加重,所以在鏈表節點數量大於8時,會將鏈表轉化爲紅黑樹進行存儲。5.查詢時間複雜度:從原來的遍歷鏈表O(n),變成遍歷紅黑樹O(logN)。