ConcurrentHashMap源碼分析

1 集合特性

對(duì)于集合框架關(guān)注點(diǎn):

  1. 集合底層實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)是什么 數(shù)組+鏈表+紅黑樹
  2. 集合中元素是否允許為空 否
  3. 是否允許重復(fù)的數(shù)據(jù) 否
  4. 是否有序(這里的有序是指讀取數(shù)據(jù)和存放數(shù)據(jù)的順序是否一致) 否
  5. 是否線程安全叙凡。 是

2 ConcurrentHashMap分析

ConcurrentHashMap 繼承AbstractMap 并實(shí)現(xiàn)了 ConcurrentMap接口

CAS算法涎跨;unsafe.compareAndSwapInt(this, valueOffset, expect, update);

CAS(Compare And Swap)汪榔,意思是如果valueOffset位置包含的值與expect值相同悦荒,則更新valueOffset位置的值為update春锋,并返回true累贤,否則不更新,返回false屹耐。

與Java8的HashMap有相通之處尸疆,底層依然由“數(shù)組”+鏈表+紅黑樹;
底層結(jié)構(gòu)存放的是TreeBin對(duì)象惶岭,而不是TreeNode對(duì)象仓技;

CAS作為知名無(wú)鎖算法,那ConcurrentHashMap就沒(méi)用鎖了么俗他?當(dāng)然不是脖捻,hash值相同的鏈表的頭結(jié)點(diǎn)還是會(huì)synchronized上鎖。

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable 

簡(jiǎn)單寫個(gè)使用

        ConcurrentHashMap concurrentHashMap=new ConcurrentHashMap();
        concurrentHashMap.put("a",123);

接下來(lái)分析下put方法兆衅,代碼太長(zhǎng)地沮,拆開一步一步分析

        if (key == null || value == null) throw new NullPointerException();//

說(shuō)明key和value都不能為空,這是和hashMap的區(qū)別

        int hash = spread(key.hashCode());//計(jì)算hash值使元素分布的更均勻
        int binCount = 0;
        for (Node<K,V>[] tab = table;;)//table即為其基本結(jié)構(gòu)

這里為啥有個(gè)for循環(huán)羡亩?
是因?yàn)樵趖able的初始化和casTabAt用到了compareAndSwapInt摩疑、compareAndSwapObject,因?yàn)槿绻渌€程正在修改tab畏铆,那么嘗試就會(huì)失敗雷袋,所以這邊要加一個(gè)for循環(huán),不斷的嘗試

接著看table的基本結(jié)構(gòu)辞居,為靜態(tài)內(nèi)部類

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;//保持內(nèi)存可見(jiàn)性

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

接著分析初始化

            if (tab == null || (n = tab.length) == 0)
                tab = initTable();

initTable
sizeCtl:

負(fù)數(shù)代表正在進(jìn)行初始化或擴(kuò)容操作

-1代表正在初始化

-N表示有N-1個(gè)線程正在進(jìn)行擴(kuò)容操作

正數(shù)或0代表hash表還沒(méi)有被初始化楷怒,這個(gè)數(shù)值表示初始化或下一次進(jìn)行擴(kuò)容的大小,類似于擴(kuò)容閾值瓦灶。它的值始終是當(dāng)前ConcurrentHashMap容量的0.75倍鸠删,這與loadfactor是對(duì)應(yīng)的。實(shí)際容量>=sizeCtl,則擴(kuò)容贼陶。

Thread.yield();標(biāo)識(shí)將線程掛起

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)///sizeCtl<0表示其他線程已經(jīng)在初始化了或者擴(kuò)容了刃泡,掛起當(dāng)前線程 
                Thread.yield(); // 線程掛起
            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;
    }

sizeCtl默認(rèn)為0巧娱,如果ConcurrentHashMap實(shí)例化時(shí)有傳參數(shù),sizeCtl會(huì)是一個(gè)2的冪次方的值烘贴。所以執(zhí)行第一次put操作的線程會(huì)執(zhí)行Unsafe.compareAndSwapInt方法修改sizeCtl為-1禁添,有且只有一個(gè)線程能夠修改成功,其它線程通過(guò)Thread.yield()讓出CPU時(shí)間片等待table初始化完成

那么怎么解決并發(fā)插入的問(wèn)題呢

        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                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;
                synchronized (f) {
        
    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);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
    

Doug Lea采用Unsafe.getObjectVolatile來(lái)獲取桨踪,也許有人質(zhì)疑老翘,直接table[index]不可以么,為什么要這么復(fù)雜馒闷?在java內(nèi)存模型中酪捡,我們已經(jīng)知道每個(gè)線程都有一個(gè)工作內(nèi)存叁征,里面存儲(chǔ)著table的副本纳账,雖然table是volatile修飾的,但不能保證線程每次都拿到table中的最新元素捺疼,Unsafe.getObjectVolatile可以直接獲取指定內(nèi)存的數(shù)據(jù)疏虫,保證了每次拿到數(shù)據(jù)都是最新的。

如果f為null啤呼,說(shuō)明table中這個(gè)位置第一次插入元素卧秘,利用Unsafe.compareAndSwapObject方法插入Node節(jié)點(diǎn)。

如果CAS成功官扣,說(shuō)明Node節(jié)點(diǎn)已經(jīng)插入翅敌,隨后addCount(1L, binCount)方法會(huì)檢查當(dāng)前容量是否需要進(jìn)行擴(kuò)容。

如果CAS失敗惕蹄,說(shuō)明有其它線程提前插入了節(jié)點(diǎn)蚯涮,自旋重新嘗試在這個(gè)位置插入節(jié)點(diǎn)。

如果f的hash值為-1卖陵,說(shuō)明當(dāng)前f是ForwardingNode節(jié)點(diǎn)遭顶,意味有其它線程正在擴(kuò)容,則一起進(jìn)行擴(kuò)容操作泪蔫。

其余情況把新的Node節(jié)點(diǎn)按鏈表或紅黑樹的方式插入到合適的位置棒旗,這個(gè)過(guò)程采用同步內(nèi)置鎖實(shí)現(xiàn)并發(fā)

    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) {
            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)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

在節(jié)點(diǎn)f上進(jìn)行同步,節(jié)點(diǎn)插入之前撩荣,再次利用tabAt(tab, i) == f判斷铣揉,防止被其它線程修改。
如果f.hash >= 0餐曹,說(shuō)明f是鏈表結(jié)構(gòu)的頭結(jié)點(diǎn)老速,遍歷鏈表,如果找到對(duì)應(yīng)的node節(jié)點(diǎn)凸主,則修改value橘券,否則在鏈表尾部加入節(jié)點(diǎn)。
如果f是TreeBin類型節(jié)點(diǎn),說(shuō)明f是紅黑樹根節(jié)點(diǎn)旁舰,則在樹結(jié)構(gòu)上遍歷元素锋华,更新或增加節(jié)點(diǎn)。
如果鏈表中節(jié)點(diǎn)數(shù)binCount >= TREEIFY_THRESHOLD(默認(rèn)是8)箭窜,則把鏈表轉(zhuǎn)化為紅黑樹結(jié)構(gòu)毯焕。

總結(jié)下put的流程
理一下put的流程:

  • 判空:null直接拋空指針異常;
  • hash:計(jì)算h=key.hashcode磺樱;調(diào)用spread計(jì)算hash=(h ^(h >>>16))& HASH_BITS
  • 遍歷table若table為空纳猫,則初始化,僅設(shè)置相關(guān)參數(shù)竹捉;
  • 計(jì)算當(dāng)前key存放位置芜辕,即table的下標(biāo)i=(n - 1) & hash;
  • 若待存放位置為null块差,casTabAt無(wú)鎖插入侵续;
  • 若是forwarding nodes(檢測(cè)到正在擴(kuò)容),則helpTransfer(幫助其擴(kuò)容)憨闰;
    else(待插入位置非空且不是forward節(jié)點(diǎn)状蜗,即碰撞了),將頭節(jié)點(diǎn)上鎖(保證了線程安全)鹉动;
  • 區(qū)分鏈表節(jié)點(diǎn)和樹節(jié)點(diǎn)轧坎,分別插入(遇到hash值與key值都與新節(jié)點(diǎn)一致的情況,只需要更新value值即可泽示。否則依次向后遍歷缸血,直到鏈表尾插入這個(gè)結(jié)點(diǎn);
  • 若鏈表長(zhǎng)度>8边琉,則treeifyBin轉(zhuǎn)樹(Note:若length<64,直接tryPresize,兩倍table.length;不轉(zhuǎn)樹)
  • addCount(1L, binCount)属百。
  • put操作共計(jì)兩次hash操作,再利用“與&”操作計(jì)算Node的存放位置变姨。

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;
    }
  • spread計(jì)算hash值族扰;
  • table不為空;
  • tabAt(i)處桶位不為空定欧;
  • check first渔呵,是則返回當(dāng)前Node的value;否則分別根據(jù)樹砍鸠、鏈表查詢扩氢。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市爷辱,隨后出現(xiàn)的幾起案子录豺,更是在濱河造成了極大的恐慌朦肘,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件双饥,死亡現(xiàn)場(chǎng)離奇詭異媒抠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)咏花,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門趴生,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人昏翰,你說(shuō)我怎么就攤上這事苍匆。” “怎么了棚菊?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵浸踩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我窍株,道長(zhǎng)民轴,這世上最難降的妖魔是什么攻柠? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任球订,我火速辦了婚禮,結(jié)果婚禮上瑰钮,老公的妹妹穿的比我還像新娘冒滩。我一直安慰自己,他們只是感情好浪谴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布开睡。 她就那樣靜靜地躺著,像睡著了一般苟耻。 火紅的嫁衣襯著肌膚如雪篇恒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天凶杖,我揣著相機(jī)與錄音胁艰,去河邊找鬼。 笑死智蝠,一個(gè)胖子當(dāng)著我的面吹牛腾么,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播杈湾,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼解虱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了漆撞?” 一聲冷哼從身側(cè)響起殴泰,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤于宙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后悍汛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體限煞,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年员凝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了署驻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡健霹,死狀恐怖旺上,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糖埋,我是刑警寧澤宣吱,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站瞳别,受9級(jí)特大地震影響征候,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜祟敛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一疤坝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧馆铁,春花似錦跑揉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至辣垒,卻和暖如春望侈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背勋桶。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工脱衙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哥遮。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓岂丘,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親眠饮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奥帘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容