java.util.concurrent源碼閱讀 06 ConcurrentMap

Java集合框架中的Map類型的數(shù)據(jù)結(jié)構(gòu)是非線程安全, 在多線程環(huán)境中使用時(shí)需要手動(dòng)進(jìn)行線程同步. 因此在java.util.concurrent包中提供了一個(gè)線程安全版本的Map類型數(shù)據(jù)結(jié)構(gòu): ConcurrentMap. 本篇文章主要關(guān)注ConcurrentMap接口以及它的Hash版本的實(shí)現(xiàn)ConcurrentHashMap.

要實(shí)現(xiàn)線程安全,就需要加鎖, HashTable就是線程安全的, 但是HashTable對(duì)整張表加鎖的做法非常消耗性能, ConcurrentMap的做法簡(jiǎn)單來(lái)說(shuō), 就是把哈希表分成若干段, 對(duì)其中某一段操作時(shí), 只鎖住這一段, 其他段可以不受影響. 如下圖所示:

ConcurrentHashMap存儲(chǔ)結(jié)構(gòu)

整個(gè)ConcurrentMap由一個(gè)segment數(shù)組組成(即segments), 數(shù)組中每一個(gè)segment是一張哈希表, 哈希表中存放的是一張hashentry鏈表.

本文主要分析以下兩個(gè)過(guò)程:

1.ConcurrentMap的構(gòu)造方法
2.put(K key, V value)

ConcurrentMap的構(gòu)造方法

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel);
public ConcurrentHashMap(int initialCapacity, float loadFactor);

public ConcurrentHashMap(int initialCapacity);

public ConcurrentHashMap();

public ConcurrentHashMap(Map<? extends K, ? extends V> m);

這5種構(gòu)造方法, 最終都會(huì)使用第一個(gè)構(gòu)造函數(shù)來(lái)初始化ConcurrentHashMap.

第一個(gè)構(gòu)造函數(shù)的代碼:

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

其中concurrencyLevel是實(shí)例化以后就不可改變的, 它決定了segments數(shù)組的大小, 同時(shí)決定了這個(gè)ConcurrentHashMap的并發(fā)能力.segmentShift和segmentMask與hash算法相關(guān).loadFactor參數(shù)決定了Segment在何時(shí)擴(kuò)容.

ConcurrentHashMap的put方法

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

過(guò)程如下:
1.通過(guò)第一次哈希算法,計(jì)算當(dāng)前key屬于哪一個(gè)segment;
2.如果映射到的segment為空, 則執(zhí)行3, 否則執(zhí)行4;
3.借助UNSAFE類, 使用線程安全的方式, 構(gòu)建一個(gè)新的segment, 并且放入segments中相應(yīng)的位置;
4.調(diào)用segment中的put方法, 將key, value對(duì)放入segment中.

從中可以看到, 最終存儲(chǔ)key,value對(duì)的是segment, 因此只要對(duì)需要插入鍵值對(duì)的segment上鎖就可以保證線程安全.

segment的put方法

首先看segment的定義, 它是ConcurrentHashMap的內(nèi)部類, 該類繼承了ReentrantLock.

static final class Segment<K,V> extends ReentrantLock implements Serializable {}

查看它的構(gòu)造方法:

Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
    this.loadFactor = lf;
    this.threshold = threshold;
    this.table = tab;
}

Segment的主要構(gòu)成是一張哈希表(存儲(chǔ)hashentry)和threshold(當(dāng)哈希表尺寸大于threshold時(shí)擴(kuò)容).

下面是它的put方法:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

put方法的主要流程如下:
1.嘗試獲取當(dāng)前segment的鎖,若成功則執(zhí)行3, 若失敗則執(zhí)行2;
2.根據(jù)key在hashtable中搜索hashentry, 并且獲取鎖(此處會(huì)進(jìn)行阻塞操作);
3.根據(jù)key在hashtable中搜索hashentry,如果當(dāng)前位置已經(jīng)存在值, 則使用鏈接的方式解決沖突;
4.如果當(dāng)前尺寸超過(guò)了threshold,則執(zhí)行rehash(將segment中的所有鍵值對(duì)重新hash).

總結(jié)

ConcurrentHashMap的實(shí)現(xiàn),主要原理就是使用segments將原本唯一的hashtable分段, 增加并發(fā)能力. ConcurrentHashMap中還有比較重要的一點(diǎn)就是使用的兩次哈希算法(第一次哈希算法找到segment, 第二次哈希算法找到hashentry), 這兩次哈希算法要求能盡量將元素均勻的分布到不同的segment的hashtable中, ConcurrentHashMap使用的是single-word Wang/Jenkins哈希算法的一個(gè)變種, 關(guān)于哈希算法, 可以看此處(http://burtleburtle.net/bob/hash/doobs.html) 進(jìn)一步了解.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末馆铁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌望门,老刑警劉巖善绎,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件息堂,死亡現(xiàn)場(chǎng)離奇詭異屎媳,居然都是意外死亡例隆,警方通過(guò)查閱死者的電腦和手機(jī)弥搞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門邮绿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人攀例,你說(shuō)我怎么就攤上這事船逮。” “怎么了粤铭?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵挖胃,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我梆惯,道長(zhǎng)酱鸭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任垛吗,我火速辦了婚禮凹髓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怯屉。我一直安慰自己蔚舀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布锨络。 她就那樣靜靜地躺著赌躺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪足删。 梳的紋絲不亂的頭發(fā)上寿谴,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音失受,去河邊找鬼讶泰。 笑死咏瑟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痪署。 我是一名探鬼主播码泞,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼狼犯!你這毒婦竟也來(lái)了余寥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悯森,失蹤者是張志新(化名)和其女友劉穎宋舷,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓢姻,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祝蝠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了幻碱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绎狭。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖褥傍,靈堂內(nèi)的尸體忽然破棺而出儡嘶,到底是詐尸還是另有隱情,我是刑警寧澤恍风,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布蹦狂,位于F島的核電站,受9級(jí)特大地震影響邻耕,放射性物質(zhì)發(fā)生泄漏鸥咖。R本人自食惡果不足惜燕鸽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一兄世、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啊研,春花似錦御滩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至沟娱,卻和暖如春氛驮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背济似。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工矫废, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盏缤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓蓖扑,卻偏偏與公主長(zhǎng)得像唉铜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子律杠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

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

  • ConcurrencyMap 從這一節(jié)開始正式進(jìn)入并發(fā)容器的部分潭流,來(lái)看看JDK 6帶來(lái)了哪些并發(fā)容器。 在JDK ...
    raincoffee閱讀 554評(píng)論 0 0
  • 還記得大學(xué)快畢業(yè)的時(shí)候要準(zhǔn)備找工作了柜去,然后就看各種面試相關(guān)的書籍灰嫉,還記得很多面試書中都說(shuō)到: HashMap是非線...
    端木軒閱讀 235評(píng)論 0 2
  • 概述 還記得大學(xué)快畢業(yè)的時(shí)候要準(zhǔn)備找工作了,然后就看各種面試相關(guān)的書籍嗓奢,還記得很多面試書中都說(shuō)到: HashMap...
    winwill2012閱讀 1,504評(píng)論 3 16
  • ConcurrentHashMap為了高并發(fā)而設(shè)計(jì)熬甫,相比于HashTable和HashMap有更多優(yōu)勢(shì)。HashT...
    加大裝益達(dá)閱讀 465評(píng)論 0 0
  • 旅游若是不做攻略蔓罚,不對(duì)人文故事有所了解椿肩,那就只能是到此一游了。 大部分人的旅游便是去過(guò)豺谈,買過(guò)那的東西郑象,拍過(guò)照片這三...
    清風(fēng)伏筆閱讀 567評(píng)論 2 12