并發(fā)編程之ConcurrentHashMap源碼解讀-1.7

上一篇文章并發(fā)編程之ConcurrentHashMap源碼解讀-1.8中拄养,我們?cè)敿?xì)解讀了JDK 1.8 中ConcurrentHashMap的源碼。既然都提到了JDK 1.8的ConcurrentHashMap弛房,那么就不得不提JDK 1.7的ConcurrentHashMap值骇,兩者的實(shí)現(xiàn)是有很大不同的莹菱。這篇文章中就讓我們來一探究竟吧!本文主要包括以下幾個(gè)部分:

  1. 前言
  2. 基本原理
  3. 基礎(chǔ)組件
  4. put
  5. 擴(kuò)容
  6. get
  7. size
  8. 總結(jié)

1. 前言

因?yàn)镃oncurrentHashMap在JDK 1.7 & 1.8中有著截然不同的實(shí)現(xiàn)吱瘩,故有必要再介紹下ConcurrentHashMap 在 JDK 1.7中的實(shí)現(xiàn)(本文基于的JDK 版本為 1.7.0_80)道伟。在進(jìn)入正文之前,老規(guī)矩使碾,先提出幾個(gè)問題:

  • Q1:為什么不使用HashTable蜜徽?
    HashTable采用了對(duì)所有可能存在線程安全的方法加synchronized鎖的方式,來實(shí)現(xiàn)線程安全票摇。鎖粒度過粗拘鞋,效率低下。
  • Q2:ConcurrentHashMap如何實(shí)現(xiàn)線程安全矢门?
    ConcurrentHashMap內(nèi)部維護(hù)了一個(gè)Segment[]盆色,而Segment繼承自ReentrantLock,put的時(shí)候祟剔,先定位到具體的Segment隔躲,然后獲取Segment鎖,只有成功獲得鎖的線程才能插入數(shù)據(jù)物延。從而利用ReentrantLock實(shí)現(xiàn)線程安全蹭越。
  • Q3:ConcurrentHashMap如何降低鎖的粒度的?
    維護(hù)多個(gè)Segment教届,插入數(shù)據(jù)的時(shí)候只鎖對(duì)應(yīng)的Segment响鹃,而不是鎖所有的Segment。
  • Q4:ConcurrentHashMap擴(kuò)容的時(shí)候支持并發(fā)讀寫嗎案训?
    擴(kuò)容的時(shí)候买置,定位到同一個(gè)Segment的寫線程會(huì)阻塞至擴(kuò)容完成。讀線程不受影響强霎,總是能通過UNSAFE.getObjectVolatile拿到最新的值忿项,并且通過volatile HashEntry 保證擴(kuò)容后的數(shù)組可見性。

2. 基本原理

  1. 內(nèi)部持有一個(gè)Segment<K,V>[]城舞,用來將鎖進(jìn)行分段轩触,從而降低鎖的粒度,提升并發(fā)性能家夺。
    1.1 Segment 繼承自ReentrantLock脱柱,保證其有鎖的特性。
    1.2 Segment 同時(shí)持有volatile HashEntry<K,V>[]拉馋,用來存儲(chǔ)相應(yīng)的key-value榨为,同時(shí)保證了HashEntry之間的可見性惨好。
    1.3 Segment<K,V>[]會(huì)在構(gòu)造函數(shù)被調(diào)用的時(shí)候初始化,擴(kuò)容的時(shí)候僅僅擴(kuò)容Segment中的HashEntry<K,V>[]随闺,這里會(huì)初始化Segment[0]日川,用來當(dāng)作初始化其它Segment的模板
  2. put的先進(jìn)行hash擾動(dòng)運(yùn)算矩乐,然后定位到具體的segment龄句,然后根據(jù)情況決定初始化segment或者請(qǐng)求鎖
    2.1 hash擾動(dòng)(目標(biāo)是充分混合hash特征散罕,減少?zèng)_突)撒璧。
    2.2 計(jì)算索引得到segment[i],如果它尚未初始化笨使,則先初始化segment[i]卿樱。
    2.3 嘗試獲取segment[i]鎖,獲取鎖失敗硫椰,則自旋或者阻塞至成功獲得鎖繁调。
    2.4 定位HashEntry<K,V>[j]更新鏈表,更新后視情況決定是否擴(kuò)容靶草。
    2.5 釋放鎖蹄胰。
  3. get的時(shí)候同樣先進(jìn)行hash擾動(dòng)運(yùn)算,然后定位到具體的segment奕翔,然后定位到具體的HashEntry<K,V>裕寨,遍歷取值。
    3.1 取具體的HashEntry<K,V>的時(shí)候派继,利用UNSAFE.getObjectVolatile保證取到的是最新的HashEntry<K,V>
  4. 統(tǒng)計(jì)size的時(shí)候宾袜,會(huì)先嘗試在不加鎖的情況下,累加各個(gè)segment的count驾窟,循環(huán)兩次庆猫。如果中途數(shù)據(jù)沒有更改,則認(rèn)為數(shù)據(jù)正確绅络,如果更改月培,則在加鎖的情況下再統(tǒng)計(jì)一次。
    [圖片上傳中...(0073Cjx6ly1gkj896whqvj3020020dfw.jpg-ef6b90-1608789320783-0)]

從上面的流程中恩急,我們可以清晰的看到ConcurrentHashMap中的數(shù)據(jù)結(jié)構(gòu)為Segment<K,V>[]+HashEntry<K,V>[]

ConcurrentHashMap 結(jié)構(gòu)圖.png

put 流程

put流程.png

3. 基礎(chǔ)組件

屬性

默認(rèn)初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
默認(rèn)加載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默認(rèn)并發(fā)級(jí)別
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
存儲(chǔ)HashEntry<K,V>[]的Segment<K,V>[]
final Segment<K,V>[] segments;

Segment

繼承自ReentrantLock杉畜,具有鎖的特性
static final class Segment<K,V> extends ReentrantLock implements Serializable {
        獲取鎖失敗時(shí)最大自旋次數(shù)
        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
        用來實(shí)際存儲(chǔ)數(shù)據(jù)的HashEntry<K,V>[]
        transient volatile HashEntry<K,V>[] table;
        當(dāng)前Segment的實(shí)際數(shù)據(jù)量
        transient int count;
        修改次數(shù)
        transient int modCount;
        閾值,用來指導(dǎo)擴(kuò)容
        transient int threshold;
        加載因子衷恭,擴(kuò)容后計(jì)算新的threshold
        final float loadFactor;

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

        省略部分方法和代碼
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            ...................
        }
}    

我們都說ConcurrentHashMap利用的是鎖分段的思想此叠,這里的鎖就是Segment,它通過繼承ReentrantLock 來獲得鎖的特性匾荆。另外一點(diǎn)需要注意的是拌蜘,調(diào)用put方法的時(shí)候杆烁,其實(shí)是調(diào)用Segment內(nèi)的put方法牙丽。關(guān)于ReentrantLock是如何實(shí)現(xiàn)鎖功能的简卧,感興趣的可以看另一篇文章并發(fā)編程之AQS探秘

HashEntry

 鏈表
 static final class HashEntry<K,V> {
        final int hash;
        final K key;
        定義為volatile保證可見性
        volatile V value;
        指向下一個(gè)節(jié)點(diǎn)的指針
        volatile HashEntry<K,V> next;

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

4. put

之前,我們梳理了put的流程烤芦。下面結(jié)合源碼看下各個(gè)部分的實(shí)現(xiàn)举娩,重點(diǎn)是思考如何保證每個(gè)步驟的線程安全

先看下構(gòu)造方法

    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;
       這里得到的ssize始終是2的N次冪构罗,因?yàn)椴捎玫氖菑?循環(huán)往左移動(dòng)一位
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        超過最大值铜涉,設(shè)置為最大值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        看能否整除
        int c = initialCapacity / ssize;
        無法整除
        if (c * ssize < initialCapacity)
            取大的
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        計(jì)算每個(gè)HashEntry[]的初始長(zhǎng)度
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        初始Segment<K,V>[0]做為初始其它Segment的模板(即可以通過s0獲得cap、loadFactor)
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        初始Segment<K,V>[]遂唧,這里初始化之后Segment<K,V>[]就不會(huì)再擴(kuò)容了芙代,擴(kuò)容的時(shí)候僅僅是擴(kuò)容Segment<K,V>[i]中的HashEntry[]
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        賦值ss[0]
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

上面可以看到Segment<K,V>[]是在構(gòu)造方法被調(diào)用的時(shí)候就初始化的,而Segment<K,V>[i](包含其內(nèi)部的HashEntry<K,V>[])則是在put的時(shí)候才會(huì)初始化盖彭。

入口

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        進(jìn)行hash擾動(dòng)纹烹,充分混合hash特征,減少?zèng)_突
        int hash = hash(key);
        計(jì)算segment索引
        int j = (hash >>> segmentShift) & segmentMask;
        取指定segment
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            初始化segment
            s = ensureSegment(j);
        調(diào)用segement.put方法
        return s.put(key, hash, value, false);
    }

put的核心邏輯其實(shí)是在Segment.put中召边。

初始化Segment[i]

    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;
        取指定Segment最新值
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            用構(gòu)造函數(shù)時(shí)初始話的ss[0]作為模板初始化ss[i]
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            容量
            int cap = proto.table.length;
            加載因子/默認(rèn)0.75
            float lf = proto.loadFactor;
            閾值铺呵,超過這個(gè)值并且HashEntry[].length小于數(shù)組最大長(zhǎng)度,則需要擴(kuò)容
            int threshold = (int)(cap * lf);
            初始話HashEntry[]
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            再次檢查是否為空隧熙,感知其它線程初始化
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                初始化Segment<K,V>
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                循環(huán)利用CAS設(shè)置ss[i]=s片挂,直到成功
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

從源碼中,我們看到了幾點(diǎn)保證線程安全的措施:1)利用UNSAFE.getObjectVolatile保證能拿到最新的Segment<K,V>[i]的值贞盯。2)利用CAS更新Segment<K,V>[i]的值音念,直到成功或者Segment<K,V>[i]不為空,這里保證更新一定成功或者不更新時(shí)可見其它線程更新的最新值,避免了多線程同時(shí)更新躏敢。

調(diào)用segment.put完成賦值

   final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            嘗試非阻塞的情況下拿鎖症昏,成功返回null
            拿鎖失敗,則自旋父丰,或者阻塞至成功獲得鎖
            這里鎖的是當(dāng)前segment[i]肝谭,故當(dāng)多個(gè)線程同時(shí)往相同的segment[i]里put時(shí),
            只有拿到鎖的線程能成功設(shè)置值蛾扇,獲取鎖失敗的線程將會(huì)自旋或者阻塞直到成功獲得鎖
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                利用臨時(shí)變量來存儲(chǔ)table攘烛,防止在中間過程中頻繁的對(duì)table進(jìn)行volatile讀寫,優(yōu)化性能
                HashEntry<K,V>[] tab = table;
                計(jì)算當(dāng)前key在HashEntry[] 中的下標(biāo)
                int index = (tab.length - 1) & hash;
                獲取對(duì)應(yīng)下標(biāo)的HashEntry<K,V>
                volatile讀镀首,保證拿到最新的HashEntry<K,V>[i]坟漱,使非volatile變量也可以有volatile語義,配合UNSAFE.putOrderedObject使用
                HashEntry<K,V> first = entryAt(tab, index);
                 循環(huán)鏈表更哄,做數(shù)據(jù)插入
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        替換已有節(jié)點(diǎn)的值
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        指向下一個(gè)節(jié)點(diǎn)芋齿,繼續(xù)遍歷
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            新鏈表插入舊鏈表的頭部
                            node = new HashEntry<K,V>(hash, key, value, first);
                        計(jì)數(shù)
                        int c = count + 1;
                        超過閾值&&小于最大長(zhǎng)度
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            擴(kuò)容
                            rehash(node);
                        else
                            更新原來的HashEntry[]
                            volatile寫腥寇,使非volatile變量也可以有volatile寫的語義
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                解鎖
                unlock();
            }
            return oldValue;
        }

在這個(gè)方法里,有幾點(diǎn)值得注意的地方:

  1. 這里利用ReentrantLock來實(shí)現(xiàn)加鎖和解鎖觅捆,從而保證線程安全赦役。多線程同時(shí)往相同的segment[i]里put時(shí),只有拿到鎖的線程能成功設(shè)置值栅炒,獲取鎖失敗的線程將會(huì)自旋或者阻塞直到成功獲得鎖掂摔。
  2. 這里更新鏈表采用的是頭插法,不同于1.8版本的尾插式赢赊。

entryAt

    static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
        return (tab == null) ? null :
            (HashEntry<K,V>) UNSAFE.getObjectVolatile
            (tab, ((long)i << TSHIFT) + TBASE);
    }

使非volatile定義的變量也能有volatile讀的語義乙漓,即能讀到最新值。
setEntryAt

   static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
                                       HashEntry<K,V> e) {
        UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
    }

使非volatile定義的變量也能有volatile寫的語義释移,即能禁止寫操作的指令重排序叭披,但是不保證內(nèi)存可見性,故要配合UNSAFE.getObjectVolatile來使用玩讳,以便能取到最新的值涩蜘。、


5. 擴(kuò)容

ConcurrentHashMap在更新HashEntry<K,V>[]之前要進(jìn)行擴(kuò)容的檢查锋边,如果當(dāng)前segment元素?cái)?shù)量操作閾值且數(shù)組長(zhǎng)度小于最大值的時(shí)候皱坛,就要進(jìn)行擴(kuò)容。擴(kuò)容主要做了兩件事:

  1. 創(chuàng)建一個(gè)原數(shù)組兩倍大小的數(shù)組豆巨。
  2. 將老數(shù)組的元素轉(zhuǎn)移到新的數(shù)組中剩辟。轉(zhuǎn)移數(shù)組的過程中,先循環(huán)找到擴(kuò)容后往扔,后續(xù)節(jié)點(diǎn)索引都保持不變的節(jié)點(diǎn)將其插入到對(duì)應(yīng)的槽位贩猎,從而達(dá)到鏈表的復(fù)用。再分別對(duì)剩余的節(jié)點(diǎn)進(jìn)行轉(zhuǎn)移萍膛。

這種處理方式可以最大限度的重用已有鏈表吭服,最好的情況是鏈表的頭部以后都保持不變,這樣可以重用整個(gè)鏈表蝗罗,當(dāng)然最壞的方式是最后一個(gè)節(jié)點(diǎn)保持不變艇棕,則需要重建除最后一個(gè)節(jié)點(diǎn)外的所有鏈表。這里有點(diǎn)類似1.8 ConcurrentHashMap中的高低位串塑,感興趣的可以去看之前的文章關(guān)于高低位的介紹并發(fā)編程之ConcurrentHashMap源碼解讀-1.8

 private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;
            新的閾值
            threshold = (int)(newCapacity * loadFactor);
            初始化一個(gè)oldTable 2倍容量的數(shù)組
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            利用(n-1)&hash計(jì)算下標(biāo)
            int sizeMask = newCapacity - 1;
            循環(huán)轉(zhuǎn)移原有鏈表
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;
                    單節(jié)點(diǎn)鏈表沼琉,直接賦值
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        最后一個(gè)后面索引保持不變的鏈表節(jié)點(diǎn)
                        HashEntry<K,V> lastRun = e;
                        lastRun對(duì)應(yīng)的索引
                        int lastIdx = idx;
                       循環(huán)找到lastRun,因?yàn)閘astRun后的索引都不變桩匪,故這里可以直接復(fù)用lastRun節(jié)點(diǎn)及其后面的節(jié)點(diǎn)打瘪,無需重新構(gòu)建鏈表
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        復(fù)用lastRun
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        處理除了lastRun之外的剩余節(jié)點(diǎn),這里要一個(gè)個(gè)遍歷,重構(gòu)鏈表
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            計(jì)算擴(kuò)容之前構(gòu)建的new Node 對(duì)應(yīng)的下標(biāo)
            int nodeIndex = node.hash & sizeMask; // add the new node
            把newTable中對(duì)應(yīng)下標(biāo)的鏈表闺骚,插入到當(dāng)前Node的尾部
            即將當(dāng)前Node插入到newTable[nodeIndex]的頭部
            node.setNext(newTable[nodeIndex]);
            重新為newTable[nodeIndex]賦值
            newTable[nodeIndex] = node;
            更改table的指向
            table = newTable;
        }

這里因?yàn)閜ut的第一步加了鎖彩扔,保證了只有一個(gè)線程在擴(kuò)容僻爽,這樣就保證了擴(kuò)容時(shí)的線程安全虫碉,同時(shí)由于HashEntry<K,V>[] table被定義成volatile,這里就保證了擴(kuò)容最后一步更改table指向?yàn)閚ewTable的時(shí)候?qū)ζ渌€程立馬可見进泼。


6. get

    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        hash擾動(dòng)
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        利用UNSAFE.getObjectVolatile取最新的Segment[u]
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
           同樣利用UNSAFE.getObjectVolatile取最新的HashEntry<K,V>[i]
           遍歷鏈表取值
            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;
    }

值得注意的是蔗衡,雖然Segment[i]未被定義成volatile纤虽,但是仍可以通過UNSAFE.getObjectVolatile乳绕,取得最新的值。HashEntry<K,V>[i]同理逼纸。

7. size

如果要統(tǒng)計(jì)ConcurrentHashMap中的元素?cái)?shù)量洋措,我們可以怎么做呢?很自然的想法就是統(tǒng)計(jì)每個(gè)Segment的count杰刽,然后累加菠发。但是這樣就行了嗎?顯然是不行的贺嫂,試想如果統(tǒng)計(jì)的過程中滓鸠,count值發(fā)生了改變,該怎么辦第喳。那么最安全的辦法就是把Segment全鎖起來糜俗,這樣其它線程就沒辦法更改里面的值,就可以放心的統(tǒng)計(jì)了曲饱。這樣是安全了悠抹,但是效率低下,那么ConcurrentHashMap是如何解決的呢扩淀?

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;
        最終返回的size
        int size;
        size超過32位
        boolean overflow; // true if size overflows 32 bits
        modCount的和
        long sum;         // sum of modCounts
        上次統(tǒng)計(jì)的modCount的和
        long last = 0L;   // previous sum
        不加鎖嘗試的次數(shù)楔敌,從-1開始,最大值等于2驻谆,總共三次
        int retries = -1; // first iteration isn't retry
        try {
            死循環(huán)統(tǒng)計(jì)
            for (;;) {  
                三次以后加鎖
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        對(duì)所有segment加鎖卵凑,這里會(huì)強(qiáng)制創(chuàng)建所有segment,創(chuàng)建并鎖住segment胜臊,可以防止其它線程調(diào)用put等操作
                        ensureSegment(j).lock(); // force creation
                }
                復(fù)位sum
                sum = 0L;
                復(fù)位size
                size = 0;
                overflow = false;
                遍歷segment數(shù)組
                for (int j = 0; j < segments.length; ++j) {
                    通過UNSAFE.getObjectVolatile保證取到最新的segment的值
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                       累加modCount
                        sum += seg.modCount;
                        int c = seg.count;
                        累加count
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                第二次循環(huán)或者之后勺卢,modCount的和與上一次一致,即沒發(fā)生變化区端,則認(rèn)為結(jié)果正確
                if (sum == last)
                    break;
                將modCount的和賦值給last值漫,用來進(jìn)行下次循環(huán)的比較
                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;
    }
  1. 循環(huán)嘗試在不加鎖的情況下,累加segment.count织盼。
  2. 兩次統(tǒng)計(jì)之間count未發(fā)生變化(判斷變化的依據(jù)是杨何,兩次循環(huán)的modCount一致)酱塔,則認(rèn)為得到正確結(jié)果,否則繼續(xù)嘗試危虱。
  3. 達(dá)到嘗試次數(shù)上限3羊娃,則為每個(gè)segment加鎖統(tǒng)計(jì)。

8. 總結(jié)

這篇文章中埃跷,我們對(duì)JDK 1.7 ConcurretHashMap的源碼進(jìn)行了分析蕊玷,下面用幾個(gè)問題對(duì)上面的內(nèi)容進(jìn)行總結(jié)。

1. ConcurrentHashMap是如何保證線程安全的弥雹?

  • 從讀(get)的層面垃帅,利用Unsafe.getObjectVolatile保證取到的是最新的Segment&HashEntry<K,V>[i],保證寫線程對(duì)讀線程的可見性剪勿,從而保證線程安全
  • 從寫(put)的層面贸诚,利用ReentrantLock 可重入鎖保證每次只有一個(gè)線程對(duì)Segment[i]做數(shù)據(jù)插入,從而保證了線程安全厕吉。
  • 從擴(kuò)容(rehash)的層面酱固,因?yàn)橹挥衟ut的時(shí)候才會(huì)調(diào)用到rehash,同樣利用ReentrantLock 可重入鎖保證線程之間的互斥性头朱,從而保證線程安全运悲。
  • 從計(jì)數(shù)(size)層面,put的時(shí)候進(jìn)行自增项钮,統(tǒng)計(jì)的時(shí)候先嘗試不加鎖統(tǒng)計(jì)班眯,當(dāng)計(jì)數(shù)期間結(jié)果改變的時(shí)候再采用ReentrantLock 可重入鎖保證統(tǒng)計(jì)期間其它線程無法更改數(shù)據(jù),從而實(shí)現(xiàn)線程安全寄纵。

2. ConcurrentHashMap什么情況下才會(huì)觸發(fā)擴(kuò)容鳖敷?

  • 當(dāng)前Segment[i]的元素?cái)?shù)量超過閾值(負(fù)載因子*初始數(shù)量),且數(shù)組長(zhǎng)度小于最大長(zhǎng)度時(shí)

3. 是否支持并發(fā)擴(kuò)容程拭?

  • 不支持定踱,擴(kuò)容的時(shí)候定位到相同Segment,準(zhǔn)備做數(shù)據(jù)更改的線程會(huì)阻塞恃鞋。

4. 擴(kuò)容的時(shí)候是否支持并發(fā)讀寫崖媚?

  • 擴(kuò)容的時(shí)候支持并發(fā)讀。
  • 擴(kuò)容的時(shí)候不支持并發(fā)寫(同一個(gè)Segment),此時(shí)其它往這個(gè)Segment寫數(shù)據(jù)的線程會(huì)阻塞恤浪。

ConcurrentHashMap作為一個(gè)并發(fā)編程的熱點(diǎn)畅哑,無論工作還是面試中,都會(huì)有很高的出現(xiàn)頻率水由。相信通過對(duì)源碼的解讀荠呐,大家都能有些許收獲。當(dāng)然,由于水平有限泥张,難免文章中有疏漏的地方呵恢,歡迎批評(píng)指正。我們下一篇文章見....

參考 java并發(fā)編程的藝術(shù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末媚创,一起剝皮案震驚了整個(gè)濱河市渗钉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钞钙,老刑警劉巖鳄橘,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異芒炼,居然都是意外死亡瘫怜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門焕议,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宝磨,“玉大人弧关,你說我怎么就攤上這事盅安。” “怎么了世囊?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵别瞭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我株憾,道長(zhǎng)蝙寨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任嗤瞎,我火速辦了婚禮墙歪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贝奇。我一直安慰自己虹菲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布掉瞳。 她就那樣靜靜地躺著毕源,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陕习。 梳的紋絲不亂的頭發(fā)上霎褐,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音该镣,去河邊找鬼冻璃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的省艳。 我是一名探鬼主播歌粥,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拍埠!你這毒婦竟也來了失驶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤枣购,失蹤者是張志新(化名)和其女友劉穎嬉探,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棉圈,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涩堤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了分瘾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胎围。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖德召,靈堂內(nèi)的尸體忽然破棺而出白魂,到底是詐尸還是另有隱情,我是刑警寧澤上岗,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布福荸,位于F島的核電站,受9級(jí)特大地震影響肴掷,放射性物質(zhì)發(fā)生泄漏敬锐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一呆瞻、第九天 我趴在偏房一處隱蔽的房頂上張望台夺。 院中可真熱鬧,春花似錦痴脾、人聲如沸颤介。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)买窟。三九已至,卻和暖如春薯定,著一層夾襖步出監(jiān)牢的瞬間始绍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工话侄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亏推,地道東北人学赛。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吞杭,于是被迫代替她去往敵國(guó)和親盏浇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

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