jdk源碼之ConCurrentHashMap源碼註釋

    併發包裏面ConcurrentHashMap是一個使用頻率,面試頻率都是用到比較高的。今天咱們就剖析一下ConCurrentHashMap的源碼。咱們仍是使用場景驅動的方式去分析。java

  1. 構建函數

        可是發現構造函數進去之後就是一個無參的構造,沒什麼特別的。node

    2.分析put流程面試

        put流程是整個ConCurrentHashMap的精華部分,裏面淋漓盡致的使用了分段加鎖的思想。緩存

    3.分析get流程安全

     Get是一個讀流程,採用的也是無鎖化方式,可是方法調的底層的unsafe的方法保證了可見性。整個ConCurrentHashMap的性能很高。併發

    

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
//剖析寫數據流程
map.put("test","zhangsan");
//剖析讀數據流程
map.get("test")

接着咱們分析一下put的方法,這個方法很是重要。體現了整個ConCurrentHashMap的設計精髓。Put點進去之後是這個方法
public V put(K key, V value) {
//重要,點進去之後繼續分析
    return putVal(key, value, false);
}


final V putVal(K key, V value, boolean onlyIfAbsent) {
//若是key或者value其中一個null,就報空指針異常
    if (key == null || value == null) throw new NullPointerException();
//取hash值,可是咱們注意的是,這兒並非直接用的key的hashCode值。
//而是調用了spred方法。
//spread裏的關鍵代碼是(h ^ (h >>> 16)) & HASH_BITS;
//用當前的hashCode值向右移了16位,而後再作異或
//這樣操做的出來的值都保留着着hashCode的高低16位的特徵,減小hash衝突
//因此咱們這兒要知道。這兒的hash值不是直接用的hashCode
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
//若是當前的tab裏面沒有數據
        if (tab == null || (n = tab.length) == 0)
//初始化table,而後進去下一輪循環
            tab = initTable();
//進入下一輪循環之後走到這兒。
//return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
//這兒就是計算index的位置,使用tabAt的方法查找對應位置的數據
//若是對應位置的數據是null,說明咱們要插入的這個位置目前尚未數據
//注意這兒使用的是unsafe的getObjectVolatile,也就是說,這個是具備可見性的
//若是併發的時候有人修改了這個值,對這個操做也是課件。
//若是有同窗看過我以前分析過《從硬件層面聊聊synchonzied和volatile》
//應該知道volatile這兒至關因而加了load屏障,在獲取數據的時候,若是在無效隊列裏面發
//現數據無效,就會從新加載。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//若是當前位置沒有元素,直接就經過cas進行插入元素。
//cas操做的代碼是
//return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
//其實這個cas操做在硬件層面是加鎖的,因此這兒實際上是線程安全的。這點你們必定
//要知道。
//那麼這兒咱們還能夠繼續分析一下,若是這兒同時兩個線程併發進來操做
//必然有一個成功的,一個是失敗了,成功的,固然就添加了數據了。
//其實這個地方分段加鎖的思想已經出來了,由於咱們分析指導
//針對表的一個位置,咱們採用的cas的機制,這個時候,只有針對同一個位置
//纔會插入失敗。可是其他的位置照樣能夠進行cas的操做。換句話說一個位置一把鎖
//由於是for循環,因此失敗了的那個確定再次循環。
//可是由於這個位置已經有數據了。因此代碼不走這兒了。
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
//若是插入成功,那麼直接就結束退出。
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
//若是是針對同一個位置插入數據,那麼代碼就會走到這兒
//走到這兒就直接加了一把鎖,而這個鎖就是針對這個位置的node
//其實這兒也就體現了分段加鎖的思想。一個Node就一把鎖
            synchronized (f) {
          //f其實就是前面要加鎖的時候,獲取出來的node
         //其實若是代碼走到這兒,表明的就是tabAt(tab, i) == f
         //可是這兒還要繼續判斷就是爲了一個嚴謹
         //由於就怕中間有人修改過這個f這個node
  if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
//若是插入的key和value
//key是同樣的而且value不爲空
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
//直接就覆蓋
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }

//代碼走到這兒,只能說明是
//key不同,只不過是巧了,雖然key不同
//可是通過hash運算之後,當前key定位了一個
//已經有數據的位置上。
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
//直接就在當前node上掛載 一個node節點
//注意,一個節點上掛載的節點超過8個
//就會變化爲紅黑樹
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
//掛載成功之後就退出
                                break;
                            }
                        }
                    }
//若是當前定位到的Node已經變成是紅黑樹
//那麼就選擇使用紅黑樹的方式插入數據
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
//若是是散列表上面掛了8個以上的節點
//那麼就把當前列表變成紅黑樹,提高查詢效率
                if (binCount >= TREEIFY_THRESHOLD)
//變成紅黑樹
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
//計算node上掛載的節點個數
    addCount(1L, binCount);
    return null;
}



private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}


上面咱們分析了寫入數據的時候加鎖的思想,接下來咱們分析一下,ConcurrentHashMap
讀數據的時候又是什麼狀況?
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //計算位置
int h = spread(key.hashCode());
//這個條件判斷,若是table裏是有數據的,而且根據當前
//key計算出來的位置也是有數據的
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {

          //若是是hash值相同
if ((eh = e.hash) == h) {
//若是是key相同
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                //那麼咱們直接返回當前的value值就行。
return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
//若是代碼走到這兒說明,要獲取的key的對應
//hash位置確實是有數據的,對應位置的key跟傳
//進來的key位置不同,因此遍歷列表
        while ((e = e.next) != null) {
//直到遍歷到 hash位置同樣,key也同樣位置。
//而後就把當前value值返回便可。
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

整個讀的操做,咱們分析實際上是都沒有加鎖的。
不過咱們注意到map裏的元素咱們是靠tabAt(tab, (n - 1) & h)這個方法獲取到的。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
//調用了Unsafe的方法
//還保證了可見性。
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

讀這個操做保證了可見性,從硬件層面說,就是加了load屏障。因此在讀數據的時候必定會嗅探一下,若是發現無效隊列裏面元素被人修改過。就會發送read消息到總線。從別的高速緩存裏或者是主內存裏讀取數據。不加鎖,可是他經過volatile讀,儘量給你保證讀到其餘線程修改的最新的值。函數

相關文章
相關標籤/搜索