4容为、ConcurrentHashMap實現(xiàn)原理

1. 各種map線程安全介紹

1.1 HashMap

HashMap是線程不安全的壁却,在并發(fā)環(huán)境下瘤礁,可能會形成環(huán)狀鏈表(擴容時可能造成阳懂,具體原因自行百度google或查看源碼分析),導(dǎo)致get操作時,cpu空轉(zhuǎn)岩调,所以巷燥,在并發(fā)環(huán)境中使用HashMap是非常危險的。

1.2 HashTable

HashTable和HashMap的實現(xiàn)原理幾乎一樣号枕,差別:

  • 1.HashTable不允許key和value為null缰揪;
  • 2.HashTable是線程安全的。
HashTable結(jié)構(gòu)圖.png

但是HashTable線程安全的策略實現(xiàn)代價卻太大堕澄,get/put所有相關(guān)操作都是synchronized的邀跃,相當于給整個哈希表加了一把大鎖,多線程訪問時候蛙紫,只要有一個線程訪問或操作該對象拍屑,那其他線程只能阻塞,相當于將所有的操作串行化坑傅。

1.3 JDK1.7的ConcurrentHashMap

  • ConcurrentHashMap采用了非常精妙的"分段鎖"策略僵驰,ConcurrentHashMap的主干是個Segment數(shù)組。
  • Segment繼承了ReentrantLock唁毒,所以它就是一種可重入鎖(ReentrantLock)蒜茴。在ConcurrentHashMap,一個Segment就是一個子哈希表浆西,Segment里維護了一個HashEntry數(shù)組粉私,并發(fā)環(huán)境下,對于不同Segment的數(shù)據(jù)進行操作是不用考慮鎖競爭的近零。
  • 所以诺核,對于同一個Segment的操作才需考慮線程同步,不同的Segment則無需考慮久信。
  • 使用ConConcurrentHashMap時候有時候會遇到跨段的問題窖杀,跨段的時候[size()、containsValue()]裙士,可能需要鎖定部分段或者全段入客,當操作結(jié)束之后,又回按照順序進行釋放每一段的鎖腿椎。注意是按照順序解鎖的桌硫。,每個Segment又包含了多個HashEntry.
image

主要結(jié)構(gòu):ReentrantLock+Segment+HashEntry
1.7的ConcurrentHashMap源碼:

JDK1.8的ConcurrentHashMap

JDK1.8的實現(xiàn)已經(jīng)拋棄了Segment分段鎖機制啃炸,利用CAS+Synchronized來保證并發(fā)更新的安全鞍泉。數(shù)據(jù)結(jié)構(gòu)采用:數(shù)組+鏈表+紅黑樹。

image

從JDK1.7版本的ReentrantLock+Segment+HashEntry肮帐,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹。

2. JDK1.8的ConcurrentHashMap

2.1 put()

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw newNullPointerException(); // 鍵或值為空,拋出異常
    // 鍵的hash值經(jīng)過計算獲得hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { // 無限循環(huán)
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0) //表為空或者表的長度為0
            // 初始化表
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash))== null) { //表不為空并且表的長度大于0训枢,并且該桶不為空
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key,value, null))) //比較并且交換值托修,如tab的第i項空則用新生成的node替換
                break;                   // no lockwhen adding to empty bin
        }
        else if ((fh = f.hash) == MOVED) //該結(jié)點的hash值為MOVED
            // 進行結(jié)點的轉(zhuǎn)移(在擴容的過程中)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) { // 加鎖同步
                if (tabAt(tab, i) == f) {//找到table表下標為i的節(jié)點
                    if (fh >= 0) { // 該table表中該結(jié)點的hash值大于0
                        // binCount賦值為1
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) { // 無限循環(huán)
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) { // 結(jié)點的hash值相等并且key也相等
                                // 保存該結(jié)點的val值
                                oldVal = e.val;
                                if (!onlyIfAbsent) // 進行判斷
                                    // 將指定的value保存至結(jié)點,即進行了結(jié)點值的更新
                                    e.val = value;
                                break;
                            }
                            // 保存當前結(jié)點
                            Node<K,V> pred = e;
                        if ((e = e.next) == null){ // 當前結(jié)點的下一個結(jié)點為空恒界,即為最后一個結(jié)點
                                // 新生一個結(jié)點并且賦值給next域
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                // 退出循環(huán)
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 結(jié)點為紅黑樹結(jié)點類型
                        Node<K,V> p;
                        // binCount賦值為2
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) { // 將hash睦刃、key、value放入紅黑樹
                            // 保存結(jié)點的val
                            oldVal = p.val;
                            if (!onlyIfAbsent) // 判斷
                                // 賦值結(jié)點value值
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) { // binCount不為0
                if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大于等于轉(zhuǎn)化為紅黑樹的閾值
                    // 進行轉(zhuǎn)化
                    treeifyBin(tab, i);
                if (oldVal != null) // 舊值不為空
                    // 返回舊值
                    return oldVal;
                break;
            }
        }
    }
    // 增加binCount的數(shù)量
    addCount(1L, binCount);
    return null;
}

說明:put函數(shù)底層調(diào)用了putVal進行數(shù)據(jù)的插入十酣,對于putVal函數(shù)的流程大體如下涩拙。

  • 判斷存儲的key、value是否為空耸采,若為空兴泥,則拋出異常,否則虾宇,進入步驟②

  • 計算key的hash值搓彻,隨后進入無限循環(huán),該無限循環(huán)可以確保成功插入數(shù)據(jù)嘱朽,若table表為空或者長度為0旭贬,則初始化table表,否則搪泳,進入步驟③

  • 根據(jù)key的hash值取出table表中的結(jié)點元素稀轨,若取出的結(jié)點為空(該桶為空),則使用CAS將key岸军、value奋刽、hash值生成的結(jié)點放入桶中。否則凛膏,進入步驟④

  • 若該結(jié)點的的hash值為MOVED杨名,則對該桶中的結(jié)點進行轉(zhuǎn)移,否則猖毫,進入步驟⑤

  • 對桶中的第一個結(jié)點(即table表中的結(jié)點)進行加鎖台谍,對該桶進行遍歷,桶中的結(jié)點的hash值與key值與給定的hash值和key值相等吁断,則根據(jù)標識選擇是否進行更新操作(用給定的value值替換該結(jié)點的value值)趁蕊,若遍歷完桶仍沒有找到hash值與key值和指定的hash值與key值相等的結(jié)點,則直接新生一個結(jié)點并賦值為之前最后一個結(jié)點的下一個結(jié)點仔役。進入步驟⑥

  • 若binCount值達到紅黑樹轉(zhuǎn)化的閾值掷伙,則將桶中的結(jié)構(gòu)轉(zhuǎn)化為紅黑樹存儲,最后又兵,增加binCount的值任柜。

  • 在putVal函數(shù)中會涉及到如下幾個函數(shù):initTable卒废、tabAt、casTabAt宙地、helpTransfer摔认、putTreeVal、treeifyBin宅粥、addCount函數(shù)参袱。下面對其中涉及到的函數(shù)進行分析。

2.2 initTable()

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) { // 無限循環(huán)
        if ((sc = sizeCtl) < 0) // sizeCtl小于0秽梅,則進行線程讓步等待
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 比較sizeCtl的值與sc是否相等抹蚀,相等則用-1替換
            try {
                if ((tab = table) == null || tab.length == 0) { // table表為空或者大小為0
                    // sc的值是否大于0,若是企垦,則n為sc环壤,否則,n為默認初始容量
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    // 新生結(jié)點數(shù)組
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 賦值給table
                    table = tab = nt;
                    // sc為n * 3/4
                    sc = n - (n >>> 2);
                }
            } finally {
                // 設(shè)置sizeCtl的值
                sizeCtl = sc;
            }
            break;
        }
    }
    // 返回table表
    return tab;
}

說明:對于table的大小竹观,會根據(jù)sizeCtl的值進行設(shè)置镐捧,如果沒有設(shè)置szieCtl的值,那么默認生成的table大小為16臭增,否則懂酱,會根據(jù)sizeCtl的大小設(shè)置table大小。

2.3 helpTransfer()

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { // table表不為空并且結(jié)點類型使ForwardingNode類型誊抛,并且結(jié)點的nextTable不為空
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) { // 條件判斷
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0) // 
                break;
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { // 比較并交換
                // 將table的結(jié)點轉(zhuǎn)移到nextTab中
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

此函數(shù)用于在擴容時將table表中的結(jié)點轉(zhuǎn)移到nextTable中列牺。

2.4 putTreeVal()

final TreeNode<K,V> putTreeVal(int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if (p == null) {
            first = root = new TreeNode<K,V>(h, k, v, null, null);
            break;
        }
        else if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.findTreeNode(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.findTreeNode(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            TreeNode<K,V> x, f = first;
            first = x = new TreeNode<K,V>(h, k, v, f, xp);
            if (f != null)
                f.prev = x;
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            if (!xp.red)
                x.red = true;
            else {
                lockRoot();
                try {
                    root = balanceInsertion(root, x);
                } finally {
                    unlockRoot();
                }
            }
            break;
        }
    }
    assert checkInvariants(root);
    return null;
}

說明:此函數(shù)用于將指定的hash、key拗窃、value值添加到紅黑樹中瞎领,若已經(jīng)添加了,則返回null随夸,否則返回該結(jié)點九默。

2.5 treeifyBin()

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) { // 表不為空
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) // table表的長度小于最小的長度
            // 進行擴容,調(diào)整某個桶中結(jié)點數(shù)量過多的問題(由于某個桶中結(jié)點數(shù)量超出了閾值宾毒,則觸發(fā)treeifyBin)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { // 桶中存在結(jié)點并且結(jié)點的hash值大于等于0
            synchronized (b) { // 對桶中第一個結(jié)點進行加鎖
                if (tabAt(tab, index) == b) { // 第一個結(jié)點沒有變化
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) { // 遍歷桶中所有結(jié)點
                        // 新生一個TreeNode結(jié)點
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null) // 該結(jié)點前驅(qū)為空
                            // 設(shè)置p為頭結(jié)點
                            hd = p;
                        else
                            // 尾節(jié)點的next域賦值為p
                            tl.next = p;
                        // 尾節(jié)點賦值為p
                        tl = p;
                    }
                    // 設(shè)置table表中下標為index的值為hd
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

說明:此函數(shù)用于將桶中的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化為紅黑樹驼修,其中,值得注意的是诈铛,當table的長度未達到閾值時乙各,會進行一次擴容操作,該操作會使得觸發(fā)treeifyBin操作的某個桶中的所有元素進行一
次重新分配幢竹,這樣可以避免某個桶中的結(jié)點數(shù)量太大耳峦。

2.6 addCount()

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { // counterCells不為空或者比較交換失敗
        CounterCell a; long v; int m;
        // 無競爭標識
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this,SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

說明:此函數(shù)主要完成binCount的值加1的操作。

2.7 get()

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 計算key的hash值
    int h = spread(key.hashCode()); 
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) { // 表不為空并且表的長度大于0并且key所在的桶不為空
        if ((eh = e.hash) == h) { // 表中的元素的hash值與key的hash值相等
            if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 鍵相等
                // 返回值
                return e.val;
        }
        else if (eh < 0) // 結(jié)點hash值小于0
            // 在桶(鏈表/紅黑樹)中查找
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) { // 對于結(jié)點hash值大于0的情況
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

說明:get()根據(jù)key的hash值來計算在哪個桶中焕毫,再遍歷桶蹲坷,查找元素驶乾,若找到則返回該結(jié)點,否則冠句,返回null轻掩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市懦底,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌罕扎,老刑警劉巖聚唐,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異腔召,居然都是意外死亡杆查,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門臀蛛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亲桦,“玉大人,你說我怎么就攤上這事浊仆】颓停” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵抡柿,是天一觀的道長舔琅。 經(jīng)常有香客問我,道長洲劣,這世上最難降的妖魔是什么备蚓? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮囱稽,結(jié)果婚禮上郊尝,老公的妹妹穿的比我還像新娘。我一直安慰自己战惊,他們只是感情好流昏,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著样傍,像睡著了一般横缔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衫哥,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天茎刚,我揣著相機與錄音,去河邊找鬼撤逢。 笑死膛锭,一個胖子當著我的面吹牛粮坞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播初狰,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼莫杈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了奢入?” 一聲冷哼從身側(cè)響起筝闹,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腥光,沒想到半個月后关顷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡武福,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年议双,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捉片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡平痰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伍纫,到底是詐尸還是另有隱情宗雇,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布翻斟,位于F島的核電站逾礁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏访惜。R本人自食惡果不足惜嘹履,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望债热。 院中可真熱鬧砾嫉,春花似錦、人聲如沸窒篱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墙杯。三九已至配并,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間高镐,已是汗流浹背溉旋。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫉髓,地道東北人观腊。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓邑闲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親梧油。 傳聞我的和親對象是個殘疾皇子苫耸,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360