JDK源碼分析(6)ConcurrentHashMap

ConcurrentHashMap源碼分析

  1. table:默認為null,初始化發(fā)生在第一次插入操作薄湿,默認大小為16的數(shù)組鸭廷,用來存儲Node節(jié)點數(shù)據(jù)枝缔,擴容時大小總是2的冪次方布疙。
  2. nextTable:默認為null,擴容時新生成的數(shù)組愿卸,其大小為原數(shù)組的兩倍灵临。
  • sizeCtl :默認為0,用來控制table的初始化和擴容操作趴荸,具體應(yīng)用在后續(xù)會體現(xiàn)出來儒溉。
  • **-1 **代表table正在初始化
  • **-N **表示有N-1個線程正在進行擴容操作
  • 其余情況:
    1、如果table未初始化赊舶,表示table需要初始化的大小睁搭。
    2、如果table初始化完成笼平,表示table的容量,默認是table大小的0.75倍舔痪,居然用這個公式算0.75(n - (n >>> 2))寓调。
  • Node:保存key,value及key的hash值的數(shù)據(jù)結(jié)構(gòu)锄码。
class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ...
}

其中value和next都用volatile修飾夺英,保證并發(fā)的可見性。

  • ForwardingNode:一個特殊的Node節(jié)點滋捶,hash值為-1痛悯,其中存儲nextTable的引用。
final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

只有table發(fā)生擴容的時候重窟,F(xiàn)orwardingNode才會發(fā)揮作用载萌,作為一個占位符放在table中表示當(dāng)前節(jié)點為null或則已經(jīng)被移動。

實例初始化

實例化ConcurrentHashMap時帶參數(shù)時巡扇,會根據(jù)參數(shù)調(diào)整table的大小扭仁,假設(shè)參數(shù)為100,最終會調(diào)整成256厅翔,確保table的大小總是2的冪次方乖坠,算法如下:

ConcurrentHashMap<String, String> hashMap = new ConcurrentHashMap<>(100);
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

注意,ConcurrentHashMap在構(gòu)造函數(shù)中只會初始化sizeCtl值刀闷,并不會直接初始化table熊泵,而是延緩到第一次put操作仰迁。

table初始化

前面已經(jīng)提到過,table初始化操作會延緩到第一次put行為顽分。但是put是可以并發(fā)執(zhí)行的徐许,Doug Lea是如何實現(xiàn)table只初始化一次的?讓我們來看看源碼的實現(xiàn)怯邪。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
//如果一個線程發(fā)現(xiàn)sizeCtl<0绊寻,意味著另外的線程執(zhí)行CAS操作成功,當(dāng)前線程只需要讓出cpu時間片
        if ((sc = sizeCtl) < 0) 
            Thread.yield(); // lost initialization race; just spin
        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默認為0悬秉,如果ConcurrentHashMap實例化時有傳參數(shù)澄步,sizeCtl會是一個2的冪次方的值。所以執(zhí)行第一次put操作的線程會執(zhí)行Unsafe.compareAndSwapInt方法修改sizeCtl為-1和泌,有且只有一個線程能夠修改成功村缸,其它線程通過Thread.yield()讓出CPU時間片等待table初始化完成。

put操作

假設(shè)table已經(jīng)初始化完成武氓,put操作采用CAS+synchronized實現(xiàn)并發(fā)插入或更新操作梯皿,具體實現(xiàn)如下。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    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);
        ...省略部分代碼
    }
    addCount(1L, binCount);
    return null;
}
  1. hash算法
static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;}
  1. table中定位索引位置县恕,n是table的大小
int index = (n - 1) & hash
  1. 獲取table中對應(yīng)索引的元素f东羹。
    Doug Lea采用Unsafe.getObjectVolatile來獲取,也許有人質(zhì)疑忠烛,直接table[index]不可以么属提,為什么要這么復(fù)雜?
    在java內(nèi)存模型中美尸,我們已經(jīng)知道每個線程都有一個工作內(nèi)存冤议,里面存儲著table的副本,雖然table是volatile修飾的师坎,但不能保證線程每次都拿到table中的最新元素恕酸,Unsafe.getObjectVolatile可以直接獲取指定內(nèi)存的數(shù)據(jù),保證了每次拿到數(shù)據(jù)都是最新的胯陋。
  2. 如果f為null蕊温,說明table中這個位置第一次插入元素,利用Unsafe.compareAndSwapObject方法插入Node節(jié)點惶岭。
  • 如果CAS成功寿弱,說明Node節(jié)點已經(jīng)插入,隨后addCount(1L, binCount)方法會檢查當(dāng)前容量是否需要進行擴容按灶。
  • 如果CAS失敗症革,說明有其它線程提前插入了節(jié)點,自旋重新嘗試在這個位置插入節(jié)點鸯旁。
  1. 如果f的hash值為-1噪矛,說明當(dāng)前f是ForwardingNode節(jié)點量蕊,意味有其它線程正在擴容,則一起進行擴容操作艇挨。
  2. 其余情況把新的Node節(jié)點按鏈表或紅黑樹的方式插入到合適的位置残炮,這個過程采用同步內(nèi)置鎖實現(xiàn)并發(fā),代碼如下:
synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                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) {
            Node<K,V> p;
            binCount = 2;
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                           value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    p.val = value;
            }
        }
    }
}

在節(jié)點f上進行同步缩滨,節(jié)點插入之前势就,再次利用tabAt(tab, i) == f判斷,防止被其它線程修改脉漏。

  1. 如果f.hash >= 0苞冯,說明f是鏈表結(jié)構(gòu)的頭結(jié)點,遍歷鏈表侧巨,如果找到對應(yīng)的node節(jié)點舅锄,則修改value,否則在鏈表尾部加入節(jié)點司忱。
  2. 如果f是TreeBin類型節(jié)點皇忿,說明f是紅黑樹根節(jié)點,則在樹結(jié)構(gòu)上遍歷元素坦仍,更新或增加節(jié)點鳍烁。
  3. 如果鏈表中節(jié)點數(shù)binCount >= TREEIFY_THRESHOLD(默認是8),則把鏈表轉(zhuǎn)化為紅黑樹結(jié)構(gòu)繁扎。

table擴容

當(dāng)table容量不足的時候老翘,即table的元素數(shù)量達到容量閾值sizeCtl,需要對table進行擴容锻离。
整個擴容分為兩部分:

  1. 構(gòu)建一個nextTable,大小為table的兩倍墓怀。
  2. 把table的數(shù)據(jù)復(fù)制到nextTable中汽纠。

這兩個過程在單線程下實現(xiàn)很簡單,但是ConcurrentHashMap是支持并發(fā)插入的傀履,擴容操作自然也會有并發(fā)的出現(xiàn)虱朵,這種情況下,第二步可以支持節(jié)點的并發(fā)復(fù)制钓账,這樣性能自然提升不少碴犬,但實現(xiàn)的復(fù)雜度也上升了一個臺階。

先看第一步梆暮,構(gòu)建nextTable服协,毫無疑問,這個過程只能只有單個線程進行nextTable的初始化啦粹,具體實現(xiàn)如下:

private final void addCount(long x, int check) {
    ...
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

通過Unsafe.compareAndSwapInt修改sizeCtl值偿荷,保證只有一個線程能夠初始化nextTable窘游,擴容后的數(shù)組長度為原來的兩倍,但是容量是原來的1.5跳纳。

節(jié)點從table移動到nextTable忍饰,大體思想是遍歷、復(fù)制的過程寺庄。

  1. 首先根據(jù)運算得到需要遍歷的次數(shù)i艾蓝,然后利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd斗塘。
  2. 如果f == null赢织,則在table中的i位置放入fwd,這個過程是采用Unsafe.compareAndSwapObjectf方法實現(xiàn)的逛拱,很巧妙的實現(xiàn)了節(jié)點的并發(fā)移動敌厘。
  3. 如果f是鏈表的頭節(jié)點,就構(gòu)造一個反序鏈表朽合,把他們分別放在nextTable的i和i+n的位置上俱两,移動完成,采用Unsafe.putObjectVolatile方法給table原位置賦值fwd曹步。
  4. 如果f是TreeBin節(jié)點宪彩,也做一個反序處理,并判斷是否需要untreeify讲婚,把處理的結(jié)果分別放在nextTable的i和i+n的位置上尿孔,移動完成,同樣采用Unsafe.putObjectVolatile方法給table原位置賦值fwd筹麸。

遍歷過所有的節(jié)點以后就完成了復(fù)制工作活合,把table指向nextTable,并更新sizeCtl為新數(shù)組大小的0.75倍 物赶,擴容完成白指。

紅黑樹構(gòu)造

注意:如果鏈表結(jié)構(gòu)中元素超過TREEIFY_THRESHOLD閾值,默認為8個酵紫,則把鏈表轉(zhuǎn)化為紅黑樹告嘲,提高遍歷查詢效率。

if (binCount != 0) {
    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    if (oldVal != null)
        return oldVal;
    break;
}

接下來我們看看如何構(gòu)造樹結(jié)構(gòu)奖地,代碼如下:

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

可以看出橄唬,生成樹節(jié)點的代碼塊是同步的,進入同步代碼塊之后参歹,再次驗證table中index位置元素是否被修改過仰楚。
1、根據(jù)table中index位置Node鏈表,重新生成一個hd為頭結(jié)點的TreeNode鏈表缸血。
2蜜氨、根據(jù)hd頭結(jié)點,生成TreeBin樹結(jié)構(gòu)捎泻,并把樹結(jié)構(gòu)的root節(jié)點寫到table的index位置的內(nèi)存中飒炎,具體實現(xiàn)如下:

TreeBin(TreeNode<K,V> b) {
    super(TREEBIN, null, null, null);
    this.first = b;
    TreeNode<K,V> r = null;
    for (TreeNode<K,V> x = b, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (r == null) {
            x.parent = null;
            x.red = false;
            r = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = r;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
                    TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    r = balanceInsertion(r, x);
                    break;
                }
            }
        }
    }
    this.root = r;
    assert checkInvariants(root);
}

主要根據(jù)Node節(jié)點的hash值大小構(gòu)建二叉樹。這個紅黑樹的構(gòu)造過程實在有點復(fù)雜笆豁,感興趣的同學(xué)可以看看源碼郎汪。

get操作

get操作和put操作相比,顯得簡單了許多闯狱。

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;
}
  1. 判斷table是否為空煞赢,如果為空,直接返回null哄孤。
  2. 計算key的hash值照筑,并獲取指定table中指定位置的Node節(jié)點,通過遍歷鏈表或則樹結(jié)構(gòu)找到對應(yīng)的節(jié)點瘦陈,返回value值凝危。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晨逝,隨后出現(xiàn)的幾起案子蛾默,更是在濱河造成了極大的恐慌,老刑警劉巖捉貌,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件支鸡,死亡現(xiàn)場離奇詭異,居然都是意外死亡趁窃,警方通過查閱死者的電腦和手機牧挣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來醒陆,“玉大人浸踩,你說我怎么就攤上這事⊥城螅” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵据块,是天一觀的道長码邻。 經(jīng)常有香客問我,道長另假,這世上最難降的妖魔是什么像屋? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮边篮,結(jié)果婚禮上己莺,老公的妹妹穿的比我還像新娘奏甫。我一直安慰自己,他們只是感情好凌受,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布阵子。 她就那樣靜靜地躺著,像睡著了一般胜蛉。 火紅的嫁衣襯著肌膚如雪挠进。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天誊册,我揣著相機與錄音领突,去河邊找鬼。 笑死案怯,一個胖子當(dāng)著我的面吹牛君旦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘲碱,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼金砍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悍汛?” 一聲冷哼從身側(cè)響起捞魁,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎离咐,沒想到半個月后谱俭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡宵蛀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年昆著,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片术陶。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡凑懂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出梧宫,到底是詐尸還是另有隱情接谨,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布塘匣,位于F島的核電站脓豪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏忌卤。R本人自食惡果不足惜扫夜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笤闯,春花似錦堕阔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至脱衙,卻和暖如春侥猬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捐韩。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工退唠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荤胁。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓瞧预,卻偏偏與公主長得像,于是被迫代替她去往敵國和親仅政。 傳聞我的和親對象是個殘疾皇子垢油,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

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