趣談ConcurrentHashMap

最近準備面試,一談到java基礎,大部分面試官上來就java數據結構素質三連:ArrayList與LinkedList區別,HashMap底層數據結構,ConcurrentHashMap爲何能保證線程安全。java

clipboard.png

剛畢業的應屆生,或者基礎很差程序員(好比:本尊,對沒錯就是我~),只瞭解皮毛,一稍微深刻就gg思密達。面試官:嗯...回頭等通知吧~ 基本一首《涼涼》送我到門外了。程序員

很差意思,扯遠了! 前兩個問題很簡單,一個數組一個鏈表。
數組順序存儲,內存連續,查詢快,插入刪除效率稍微低(System.copyArray),不過如今略有改善。
鏈表插入刪除快速高效,查詢效率差了點意思,存儲不連續。
總之,各有利弊吧,根據業務場景選擇適合本身的存儲結構,不過如今也出現不少相似的改進版本,暫時不談了(其實我也沒了解過,啊哈哈哈~有點尷尬)
HashMap JDK1.8之前基本都是數組+鏈表實現,JDK1.8開始改成數組+列表,當列表長度大於某個值(具體忘了),鏈表轉化爲一個X爆了的數據結構————紅黑樹(我都嚇尿了反正,看了幾百遍沒記住這玩意各類算法)面試

其實今天主要是想聊一下這個叫作ConcurrentHashMap的數據結構,看過網上幾篇文章實在是看的蛋疼,一來寫的通常,對於源碼的複製粘貼,最爲我看起來吃力;二來紅黑樹太難,看着難受的一比。是在沒法理解這個數據結構的精髓所在,故而想本身寫篇文章來記錄本身學習的過程,就比如孫悟空去了一趟五指山下,作個標記!算法

廢話少說直接先上jb:數組

clipboard.png

如圖所示,相比傳統HashMap,jdk1.8以前 ConcurrentHashMap 在傳統HashEntry以前增長了一個segment數組。Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,Segment數組中每個元素就是一把鎖,每個Segment元素存儲的是HashEntry數組+鏈表。而在jdk1.8開始,ConcurrentHashMap是由CAS和Synchronized的方式去實現高併發下的線程安全。安全

咱們主要從的get,put等方法來學習ConcurrentHashMap,是如何插入和獲取元素,以及如何保證線程安全。
先看下get方法源碼:數據結構

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

我看上面的代碼好多中間變量,很影響我這種菜鳥分析邏輯,因而我按照本身的編碼風格,重寫了一下:併發

public V get(Object key) {
        int h = (key.hashCode() ^ (key.hashCode() >>> 16)) & 0x7fffffff;// 2 ^31 -1
        Node<K,V>[] tab = table;
        // 通常對哈希表的散列很天然地會想到用hash值對length取模(即除法散列法)
        // Hashtable中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,
        // 但取模會用到除法運算,效率很低,HashMap中則經過h&(length-1)的方法來代替取模,
        // 一樣實現了均勻的散列,但效率要高不少,這也是HashMap對Hashtable的一個改進。
        Node<K,V> e = tabAt(tab, (tab.length - 1) & h);
        if (tab == null || tab.length <= 0 ||e == null) {
            return null;
        }
        if (e.hash == h) {
            if (e.getKey() == key || (e.getKey() != null && key.equals(e.getKey()))){
                return e.getValue();                
            }
        } else if (e.hash < 0) {
            Node<K,V> p = e.find(h, key);
            return p!= null ? p.getValue() : null;
        }
        e = e.next;
        while (e != null) {
            if (e.hash != h) {
                return null;
            }
            if (e.getKey() == key || (e.getKey() != null && key.equals(e.getKey())))
                return e.getValue();
        }
        return null;
    }
int h = (key.hashCode() ^ (key.hashCode() >>> 16)) & 0x7fffffff;// 2 ^31 -1

代碼的意思————經過哈希值二進制異或該哈希值二進制右移動16位 是爲了計算哈希值 再和 上面那玩意進行與運算並不知道是什麼鬼。以下圖:高併發

clipboard.png

計算出Hash值以後要經過hash值找到對應數組的下標進而找到數組元素:學習

Node<K,V> e = tabAt(tab, (tab.length - 1) & h);

(tab.length - 1) & h 根據計算出來的hash值從HashMap的「骨幹」——bucket數組找到對應的bucket
java.util.HashMap (ConcurrentHashMap一樣)保證bucket數組的長度是2的冪方,因此原本應該寫成:
index = n % length的,變爲能夠寫成:index = n & (length - 1) ,「&」效率會高一點。

說了這麼多咱們來看下tabAt方法:

public static int numberOfLeadingZeros(int i) {
    // HD, Figure 5-6
    if (i == 0)
        return 32;
    int n = 1;
    if (i >>> 16 == 0) { n += 16; i <<= 16; }
    if (i >>> 24 == 0) { n +=  8; i <<=  8; }
    if (i >>> 28 == 0) { n +=  4; i <<=  4; }
    if (i >>> 30 == 0) { n +=  2; i <<=  2; }
    n -= i >>> 31;
    return n;
}
U = sun.misc.Unsafe.getUnsafe(); // 獲取unsafe類的實例 單例模式
@CallerSensitive
public static Unsafe getUnsafe() {
    Class arg = Reflection.getCallerClass();//獲取調用者方法的類
    if (!VM.isSystemDomainLoader(arg.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
相關文章
相關標籤/搜索