Java集合 —— ConcurrentHashMap源碼筆記

簡(jiǎn)介

HashMap是線程不安全的股冗,所以 Java 還提供了 ConcurrentHashMap 類來解決高并發(fā)下的安全問題州袒。

Java8 中,ConcurrentHashMap 相對(duì)于 Java7 來說纺非, 它是通過 CAS 操作和 synchronized 來實(shí)現(xiàn)線程安全的须蜗, 而 Java7 是使用分段鎖來實(shí)現(xiàn)線程安全的,并且Java7是對(duì)數(shù)組分段同步蓉坎,而 Java8 是對(duì)數(shù)組元素同步澳眷。

這里大致看下的它的源碼,ConcurrentHashMap 相對(duì)于 HashMap 來說蛉艾,因?yàn)樗幚淼牟l(fā)的情況钳踊,所以源碼會(huì)復(fù)雜不少,這里做個(gè)大致了解與 HashMap 做個(gè)對(duì)比勿侯。

依然從 put 開始了解 ConcurrentHashMap 是如何實(shí)現(xiàn)線程安全的拓瞪。

源碼分析 Java8

put 方法

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    
    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        //這里可以直接發(fā)現(xiàn)一個(gè)和 HashMap 不同的地方, ConcurrentHashMap 不支持含 null 的鍵值對(duì)
        if (key == null || value == null) throw new NullPointerException();
        //計(jì)算哈希值
        int hash = spread(key.hashCode());
        //這個(gè)值將用來記錄鏈表或者紅黑樹長(zhǎng)度
        int binCount = 0;
        //此處開始自旋
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //如果數(shù)組不存在或者長(zhǎng)度為 0,則初始化數(shù)組
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //如果計(jì)算出來的位置在數(shù)組中還沒有存放對(duì)象助琐,那么通過 CAS 來放入
                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)
                //由于可能在高并發(fā)的情況下祭埂, 所以正在擴(kuò)容的時(shí)候也要考慮,這里會(huì)幫助擴(kuò)容
                tab = helpTransfer(tab, f);
            else {
                //如果該位置不為空兵钮,則可能有鏈表或者紅黑樹
                V oldVal = null;
                //使用 synchronized 同步該位置下對(duì)應(yīng)的數(shù)組里的對(duì)象
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                        //如果存在鏈表
                            binCount = 1;
                            //遍歷鏈表
                            for (Node<K,V> e = f;; ++binCount) {
                                //接來下的邏輯和 HashMap 一樣蛆橡,在鏈表尾部插入新對(duì)象
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                        //如果當(dāng)前塊里是紅黑樹
                            Node<K,V> p;
                            binCount = 2;
                            //此處遍歷紅黑樹
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                //如果 key 相同, 那么根據(jù) onlyIfAbsent 判斷是否需要替換                           
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 鏈表節(jié)點(diǎn)大于8 轉(zhuǎn)成紅黑樹
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //擴(kuò)容的邏輯也在這里面
        addCount(1L, binCount);
        return null;
    }

ConcurrentHashMap 的 put 方法掘譬,首先自旋泰演,如果存放對(duì)象的塊中為 null 時(shí),將通過 CAS 來放入新鍵值對(duì)葱轩,如果已經(jīng)存在對(duì)象的話睦焕,則使用 synchronized 給插入操作上鎖藐握。

如果發(fā)現(xiàn)正在擴(kuò)容,那么將會(huì)利用并發(fā)的特性垃喊,來幫助擴(kuò)容猾普。

補(bǔ)上初始化數(shù)組的方法 initTable

initTable 方法

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        //不斷循環(huán)直到數(shù)組建立
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                //如果 sizeCtl 小于 0,則說明有其他地方先初始化數(shù)組本谜,所以放棄執(zhí)行權(quán)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            //不然就通過 CAS 來修改 sizeCtl 值為 -1 初家,告訴其它線程數(shù)組正在初始化
                try {
                    //接下來就是創(chuàng)建一個(gè)數(shù)組
                    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 {
                    //創(chuàng)建完數(shù)組后就將 sizeCtl 修改
                    sizeCtl = sc;
                }
                break;
            }
        }
        //返回創(chuàng)建好的數(shù)組
        return tab;
    }

可以看出 ConcurrentHashMap 初始化數(shù)組的核心是 sizeCtl 這個(gè)值,這個(gè)值是 volatile 修飾的耕突,保證了它的可見性笤成,通過判斷這個(gè)值是不是小于 0,來決定是否需要執(zhí)行數(shù)組的初始化眷茁。

最后

從這兩段源碼可以看出 ConcurrentHashMap 是怎么樣實(shí)現(xiàn)一個(gè)線程安全的 HashMap 了。

這里引出一下 HashTable 為什么被棄用的問題纵诞。

HashTable 與 HashMap 的不同

  • 不支持 null 鍵值對(duì)上祈,HashMap 可以。
  • 線程安全浙芙,是對(duì)修改過程同步實(shí)現(xiàn)的登刺,這樣效率會(huì)很低。而 HashMap 不是線程安全嗡呼。
  • 初始容量是 11纸俭, 擴(kuò)容是 2 * oldCap + 1, HashMap 為 16,2 * oldCap南窗。
  • 初始容量即擴(kuò)容大小不一樣揍很,所以計(jì)算 index 的值方法也不一樣。

為什么棄用 HashTable

原因主要出在上面第二點(diǎn)万伤,它的修改是對(duì)整個(gè)方法同步實(shí)現(xiàn)的窒悔,效率會(huì)低非常多,同時(shí)它與 HashMap 還是有許多不同的地方敌买, ConcurrentHashMap 在容量以及擴(kuò)容規(guī)則等都延續(xù)了 HashMap简珠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市虹钮,隨后出現(xiàn)的幾起案子聋庵,更是在濱河造成了極大的恐慌,老刑警劉巖芙粱,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祭玉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宅倒,警方通過查閱死者的電腦和手機(jī)攘宙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門屯耸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹭劈,你說我怎么就攤上這事疗绣。” “怎么了铺韧?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵多矮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我哈打,道長(zhǎng)塔逃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任料仗,我火速辦了婚禮湾盗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘立轧。我一直安慰自己格粪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布氛改。 她就那樣靜靜地躺著帐萎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胜卤。 梳的紋絲不亂的頭發(fā)上疆导,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音葛躏,去河邊找鬼澈段。 笑死,一個(gè)胖子當(dāng)著我的面吹牛紫新,可吹牛的內(nèi)容都是我干的均蜜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼芒率,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼囤耳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起偶芍,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤充择,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后匪蟀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椎麦,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年材彪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了观挎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琴儿。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖嘁捷,靈堂內(nèi)的尸體忽然破棺而出造成,到底是詐尸還是另有隱情,我是刑警寧澤雄嚣,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布晒屎,位于F島的核電站,受9級(jí)特大地震影響缓升,放射性物質(zhì)發(fā)生泄漏鼓鲁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一港谊、第九天 我趴在偏房一處隱蔽的房頂上張望骇吭。 院中可真熱鬧,春花似錦歧寺、人聲如沸绵跷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荆残,卻和暖如春奴艾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背内斯。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工蕴潦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俘闯。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓潭苞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親真朗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子此疹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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