ConcurrentHashMap源碼分析

前言

JDK中的Hashtable是一個(gè)線程安全的K-V形式的容器,它實(shí)現(xiàn)線程安全的原理十分簡(jiǎn)單恩脂,就是在所有涉及對(duì)該哈希表操作的方法上都加上了synchronized關(guān)鍵字,進(jìn)行加鎖操作趣斤。這么做實(shí)現(xiàn)了線程安全俩块,但是效率非常低。

//通過synchronized在每次進(jìn)入方法時(shí)獲取hashmap的鎖浓领,高并發(fā)情況下會(huì)出現(xiàn)爭(zhēng)用沖突
public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }

ConcurrentHashMap使用了分段鎖的設(shè)計(jì)玉凯,只有在每個(gè)段上才會(huì)存在并發(fā)爭(zhēng)用,不同的段之間不存在鎖的競(jìng)爭(zhēng)联贩,提高了該hashmap的并發(fā)訪問效率漫仆。但也是由于ConcurrentHashMap的分段設(shè)計(jì),某些需要掃描所有段的操作如size()等方法實(shí)現(xiàn)上比較復(fù)雜撑蒜,并且并不能保證強(qiáng)一致性歹啼,這個(gè)在某些情況下需要注意。由于ConcurrentHashMap在各個(gè)JDK版本下的實(shí)現(xiàn)是有區(qū)別的座菠,特說(shuō)明本文基于jdk1.7源碼狸眼,我們下面一起通過其源碼來(lái)分析它的實(shí)現(xiàn)原理。

ConcurrentHashMap的組成

查看源碼浴滴,我們可以通過ConcurrentHashMap的成員變量拓萌,了解該數(shù)據(jù)結(jié)構(gòu)的基本組成。

    final int segmentMask;//作為查找segments的掩碼升略,前幾個(gè)bit用來(lái)選擇segment

    final int segmentShift;

    final Segment<K,V>[] segments;//每個(gè)segment都是一個(gè)hashtable 

ConcurrentHashMap內(nèi)部有一個(gè)segments數(shù)組微王,每個(gè)segment都是一個(gè)hashtable ,由于Segment繼承自ReentrantLock 品嚣,因此Segment本身就是鎖炕倘,對(duì)每個(gè)Segment的加鎖就是調(diào)用自身的acquire()方法。Segment的基本組成如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
        transient volatile HashEntry<K,V>[] table;//存入ConcurrentHashMap的k-v數(shù)據(jù)都存在這里
        transient int count;//該segment內(nèi)的HashEntry計(jì)數(shù)(如put/remove/replace/clear)
        transient int modCount;//對(duì)該segment的修改操作數(shù)量
        final float loadFactor;//hashtable的負(fù)載因子
. . . . . . . . .
}

HashEntry是最終存儲(chǔ)每一對(duì)k-v的最底層數(shù)據(jù)結(jié)構(gòu)翰撑,它的結(jié)構(gòu)為:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
}

value為volatile罩旋,在多線程環(huán)境下可以保持可見性,因此可以不加鎖直接讀取眶诈。
ConcurrentHashMap的整體數(shù)據(jù)結(jié)構(gòu)如圖


ConcurrentHashMap源碼分析

本文前言中涨醋,我們說(shuō)了‘ConcurrentHashMap通過分段鎖技術(shù)提高了該hashmap的并發(fā)訪問效率’,我們接下來(lái)就通過ConcurrentHashMap的get/set/remove等方法來(lái)說(shuō)明逝撬,ConcurrentHashMap在保證線程安全的情況下浴骂,是如何做到這些的。

  • ConcurrentHashMap的初始化
    concurrencyLevel 是預(yù)估會(huì)對(duì)ConcurrentHashMap進(jìn)行并發(fā)修改的線程數(shù)宪潮,也可以理解為支持的最大并發(fā)訪問ConcurrentHashMap且不產(chǎn)生鎖的爭(zhēng)用的線程數(shù)溯警,也就是Segment的數(shù)量趣苏,也即分段鎖的個(gè)數(shù),默認(rèn)為16愧膀。
//loadFactor是每個(gè)segment的負(fù)載因子拦键,concurrencyLevel是估計(jì)的并發(fā)修改線程,默認(rèn)為16檩淋,所以創(chuàng)建16個(gè)segment
  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;//默認(rèn)16
        // 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]);//初始化一個(gè)segment
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
  • 創(chuàng)建分段鎖
    如上面ConcurrentHashMap的初始化芬为,ConcurrentHashMap采用延遲初始化的模式,首先只new一個(gè)Segment蟀悦,剩下的Segment只在使用時(shí)初始化媚朦。每次put操作時(shí),定位到key所在的Segment后日戈,會(huì)先去判斷Segment是否初始化了,沒有的話由新建一個(gè)Segment并通過循環(huán)CAS設(shè)置Segment數(shù)組對(duì)應(yīng)位置引用询张。
 private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
  • get()
    get方法不需要加鎖操作,因?yàn)镠ashEntry中的value是volatile浙炼,可以保證線程間的可見性份氧。由于segments非volatile,通過UNSAFE的getObjectVolatile方法提供volatile讀語(yǔ)義來(lái)遍歷獲得對(duì)應(yīng)鏈表上的節(jié)點(diǎn)弯屈。但沒有鎖可能會(huì)導(dǎo)致在遍歷 的過程中幾點(diǎn) 被其它線程修改蜗帜,返回的val可能是過時(shí)數(shù)據(jù),這部分是ConcurrentHashMap非強(qiáng)一致性的體現(xiàn)资厉,如果對(duì)強(qiáng)一致性有要求厅缺,那么就只能去使用Hashtable或Collections.synchronizedMap這種通過synchronized關(guān)鍵字進(jìn)行加鎖提供強(qiáng)一致性的Map容器。containsKey操作與get是 一樣 的宴偿。
public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//u是該key在segments數(shù)組的下標(biāo)湘捎,可以定位該key是在哪一個(gè)Segment
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
//(tab.length - 1) & h)定位key在Segment的HashEntry數(shù)組中的下標(biāo)
//遍歷HashEntry鏈表,找到與key和key的hash值一致的HashEntry元素
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }
  • put/putIfAbsent/putAll操作
    數(shù)據(jù)插入操作窄刘,在這里才能看到ConcurrentHashMap基于分段鎖提高高并發(fā)環(huán)境下的處理能力窥妇,增大ConcurrentHashMap吞吐量的實(shí)現(xiàn)技巧。
//該方法是ConcurrentHashMap所有put操作的核心
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//首先嘗試加鎖(調(diào)用ReentrantLock的tryLock方法娩践,對(duì)當(dāng)前Segment實(shí)例加鎖)秩伞,若沒有獲取成功則找到與該Key相同的Node,若沒有則new一個(gè)。返回時(shí)必須獲取鎖欺矫,
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;//
        int index = (tab.length - 1) & hash;//當(dāng)前該Node的插入點(diǎn)
        HashEntry<K,V> first = entryAt(tab, index);//獲取tbale中第index個(gè)元素
        //在鏈表上遍歷對(duì)比節(jié)點(diǎn)有相同的key則修改對(duì)應(yīng)value,沒有則放在鏈表尾部
        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) {//key不存在的情況下才會(huì)設(shè)置value
                        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;
                //當(dāng)當(dāng)前segment的元素?cái)?shù)量超過閾值時(shí)rehash
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);//超過閾值進(jìn)行擴(kuò)容
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}
}
  • size()/containsValue()
    先不加鎖進(jìn)行循環(huán)相加計(jì)算展氓,最少進(jìn)行兩次對(duì)所有Segment的modCount與count進(jìn)行求和穆趴,如果前后兩次modCount求和的值相等,認(rèn)為這期間ConcurrentHashMap的HashEntry數(shù)量沒有變化遇汞,就 直接返回count求和的值未妹,認(rèn)為這就是ConcurrentHashMap的大小簿废。如果前后兩次modCount求和的值相等一直不相等,會(huì)循環(huán)進(jìn)行 這樣 的嘗試络它。默認(rèn)是兩次族檬,超過兩次就會(huì)認(rèn)為可能現(xiàn)在在ConcurrentHashMap上正在 進(jìn)行著密集的操作(會(huì) 影響ConcurrentHashMap的大小)化戳,于是會(huì)強(qiáng)制對(duì) 每個(gè)Segment進(jìn)行加鎖单料,然后在重復(fù) 前面的計(jì)數(shù)操作,最終返回 計(jì)算的ConcurrentHashMap的大小点楼。因此多線程環(huán)境下這種實(shí)現(xiàn)返回 的 ConcurrentHashMap大小是近似的扫尖,不準(zhǔn)確的,這個(gè) 需要在使用ConcurrentHashMap時(shí)做出取舍掠廓。containsValue與size操作原理類似换怖。
public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)//判斷前后modCount和相等
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

總結(jié)

以上就是jdk1.7中ConcurrentHashMap重要操作的源碼實(shí)現(xiàn)分析。我們可以對(duì) ConcurrentHashMap做出 以下判斷:

  • 通過分段鎖的設(shè)計(jì)蟀瞧,減小了各個(gè)分段之間的鎖的爭(zhēng)用沖突沉颂,各個(gè)分段本省 就是一個(gè)獨(dú)立的哈希表。這樣的設(shè)計(jì)提高 了ConcurrentHashMap的 吞吐量悦污,提高了hashmap的并發(fā)訪問效率铸屉。
  • HashEntry中value是volatile類型,使得ConcurrentHashMap的get方法可以無(wú)鎖操作獲取value塞关,提高了get的效率抬探。但需要注意的是,這也導(dǎo)致了ConcurrentHashMap的 弱一致性問題帆赢,在對(duì)get操作 有強(qiáng)一致性要求的場(chǎng)景下小压,必須選用其它支持map強(qiáng)一致性的容器。
  • size等操作在多線程環(huán)境下的返回值是不精確的椰于,只可做近似值怠益,建議多線程環(huán)境下不要依賴size()。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘾婿,一起剝皮案震驚了整個(gè)濱河市蜻牢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌偏陪,老刑警劉巖抢呆,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異笛谦,居然都是意外死亡抱虐,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門饥脑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)恳邀,“玉大人懦冰,你說(shuō)我怎么就攤上這事∫シ校” “怎么了刷钢?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)乳附。 經(jīng)常有香客問我内地,道長(zhǎng),這世上最難降的妖魔是什么许溅? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任瓤鼻,我火速辦了婚禮,結(jié)果婚禮上贤重,老公的妹妹穿的比我還像新娘茬祷。我一直安慰自己,他們只是感情好并蝗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布祭犯。 她就那樣靜靜地躺著,像睡著了一般滚停。 火紅的嫁衣襯著肌膚如雪沃粗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天键畴,我揣著相機(jī)與錄音最盅,去河邊找鬼。 笑死起惕,一個(gè)胖子當(dāng)著我的面吹牛涡贱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播惹想,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼问词,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了嘀粱?” 一聲冷哼從身側(cè)響起激挪,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锋叨,沒想到半個(gè)月后垄分,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡娃磺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年锋喜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嘿般,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涯冠,到底是詐尸還是另有隱情炉奴,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布蛇更,位于F島的核電站瞻赶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏派任。R本人自食惡果不足惜砸逊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掌逛。 院中可真熱鬧师逸,春花似錦、人聲如沸豆混。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皿伺。三九已至员辩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸵鸥,已是汗流浹背奠滑。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妒穴,地道東北人宋税。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像宰翅,于是被迫代替她去往敵國(guó)和親弃甥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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