溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

死磕 java集合之ConcurrentHashMap源碼分析(一)

發(fā)布時間:2020-08-10 23:38:48 來源:網(wǎng)絡(luò) 閱讀:279 作者:彤哥讀源碼 欄目:編程語言

前記,從這篇文章開始我們換一種學(xué)習(xí)的方式,彤哥先拋出問題,大家嘗試著在腦海中回答這些問題,然后再進(jìn)入我們的源碼分析過程,最后彤哥再挑幾個問題回答。

開篇問題

(1)ConcurrentHashMap與HashMap的數(shù)據(jù)結(jié)構(gòu)是否一樣?

(2)HashMap在多線程環(huán)境下何時會出現(xiàn)并發(fā)安全問題?

(3)ConcurrentHashMap是怎么解決并發(fā)安全問題的?

(4)ConcurrentHashMap使用了哪些鎖?

(5)ConcurrentHashMap的擴(kuò)容是怎么進(jìn)行的?

(6)ConcurrentHashMap是否是強(qiáng)一致性的?

(7)ConcurrentHashMap不能解決哪些問題?

(8)ConcurrentHashMap中有哪些不常見的技術(shù)值得學(xué)習(xí)?

簡介

ConcurrentHashMap是HashMap的線程安全版本,內(nèi)部也是使用(數(shù)組 + 鏈表 + 紅黑樹)的結(jié)構(gòu)來存儲元素。

相比于同樣線程安全的HashTable來說,效率等各方面都有極大地提高。

各種鎖簡介

這里先簡單介紹一下各種鎖,以便下文講到相關(guān)概念時能有個印象。

(1)synchronized

java中的關(guān)鍵字,內(nèi)部實現(xiàn)為監(jiān)視器鎖,主要是通過對象監(jiān)視器在對象頭中的字段來表明的。

synchronized從舊版本到現(xiàn)在已經(jīng)做了很多優(yōu)化了,在運(yùn)行時會有三種存在方式:偏向鎖,輕量級鎖,重量級鎖。

偏向鎖,是指一段同步代碼一直被一個線程訪問,那么這個線程會自動獲取鎖,降低獲取鎖的代價。

輕量級鎖,是指當(dāng)鎖是偏向鎖時,被另一個線程所訪問,偏向鎖會升級為輕量級鎖,這個線程會通過自旋的方式嘗試獲取鎖,不會阻塞,提高性能。

重量級鎖,是指當(dāng)鎖是輕量級鎖時,當(dāng)自旋的線程自旋了一定的次數(shù)后,還沒有獲取到鎖,就會進(jìn)入阻塞狀態(tài),該鎖升級為重量級鎖,重量級鎖會使其他線程阻塞,性能降低。

(2)CAS

CAS,Compare And Swap,它是一種樂觀鎖,認(rèn)為對于同一個數(shù)據(jù)的并發(fā)操作不一定會發(fā)生修改,在更新數(shù)據(jù)的時候,嘗試去更新數(shù)據(jù),如果失敗就不斷嘗試。

(3)volatile(非鎖)

java中的關(guān)鍵字,當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。(這里牽涉到j(luò)ava內(nèi)存模型的知識,感興趣的同學(xué)可以自己查查相關(guān)資料)

volatile只保證可見性,不保證原子性,比如 volatile修改的變量 i,針對i++操作,不保證每次結(jié)果都正確,因為i++操作是兩步操作,相當(dāng)于 i = i +1,先讀取,再加1,這種情況volatile是無法保證的。

(4)自旋鎖

自旋鎖,是指嘗試獲取鎖的線程不會阻塞,而是循環(huán)的方式不斷嘗試,這樣的好處是減少線程的上下文切換帶來的開鎖,提高性能,缺點是循環(huán)會消耗CPU。

(5)分段鎖

分段鎖,是一種鎖的設(shè)計思路,它細(xì)化了鎖的粒度,主要運(yùn)用在ConcurrentHashMap中,實現(xiàn)高效的并發(fā)操作,當(dāng)操作不需要更新整個數(shù)組時,就只鎖數(shù)組中的一項就可以了。

(5)ReentrantLock

可重入鎖,是指一個線程獲取鎖之后再嘗試獲取鎖時會自動獲取鎖,可重入鎖的優(yōu)點是避免死鎖。

其實,synchronized也是可重入鎖。

源碼分析

構(gòu)造方法

public ConcurrentHashMap() {
}

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
            MAXIMUM_CAPACITY :
            tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

構(gòu)造方法與HashMap對比可以發(fā)現(xiàn),沒有了HashMap中的threshold和loadFactor,而是改用了sizeCtl來控制,而且只存儲了容量在里面,那么它是怎么用的呢?官方給出的解釋如下:

(1)-1,表示有線程正在進(jìn)行初始化操作

(2)-(1 + nThreads),表示有n個線程正在一起擴(kuò)容

(3)0,默認(rèn)值,后續(xù)在真正初始化的時候使用默認(rèn)容量

(4)> 0,初始化或擴(kuò)容完成后下一次的擴(kuò)容門檻

至于,官方這個解釋對不對我們后面再討論。

添加元素

public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key和value都不能為null
    if (key == null || value == null) throw new NullPointerException();
    // 計算hash值
    int hash = spread(key.hashCode());
    // 要插入的元素所在桶的元素個數(shù)
    int binCount = 0;
    // 死循環(huán),結(jié)合CAS使用(如果CAS失敗,則會重新取整個桶進(jìn)行下面的流程)
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            // 如果桶未初始化或者桶個數(shù)為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)))
                // 如果使用CAS插入元素時,發(fā)現(xiàn)已經(jīng)有元素了,則進(jìn)入下一次循環(huán),重新操作
                // 如果使用CAS插入元素成功,則break跳出循環(huán),流程結(jié)束
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            // 如果要插入的元素所在的桶的第一個元素的hash是MOVED,則當(dāng)前線程幫忙一起遷移元素
            tab = helpTransfer(tab, f);
        else {
            // 如果這個桶不為空且不在遷移元素,則鎖住這個桶(分段鎖)
            // 并查找要插入的元素是否在這個桶中
            // 存在,則替換值(onlyIfAbsent=false)
            // 不存在,則插入到鏈表結(jié)尾或插入樹中
            V oldVal = null;
            synchronized (f) {
                // 再次檢測第一個元素是否有變化,如果有變化則進(jìn)入下一次循環(huán),從頭來過
                if (tabAt(tab, i) == f) {
                    // 如果第一個元素的hash值大于等于0(說明不是在遷移,也不是樹)
                    // 那就是桶中的元素使用的是鏈表方式存儲
                    if (fh >= 0) {
                        // 桶中元素個數(shù)賦值為1
                        binCount = 1;
                        // 遍歷整個桶,每次結(jié)束binCount加1
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                // 如果找到了這個元素,則賦值了新值(onlyIfAbsent=false)
                                // 并退出循環(huán)
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                // 如果到鏈表尾部還沒有找到元素
                                // 就把它插入到鏈表結(jié)尾并退出循環(huán)
                                pred.next = new Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        // 如果第一個元素是樹節(jié)點
                        Node<K,V> p;
                        // 桶中元素個數(shù)賦值為2
                        binCount = 2;
                        // 調(diào)用紅黑樹的插入方法插入元素
                        // 如果成功插入則返回null
                        // 否則返回尋找到的節(jié)點
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            // 如果找到了這個元素,則賦值了新值(onlyIfAbsent=false)
                            // 并退出循環(huán)
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 如果binCount不為0,說明成功插入了元素或者尋找到了元素
            if (binCount != 0) {
                // 如果鏈表元素個數(shù)達(dá)到了8,則嘗試樹化
                // 因為上面把元素插入到樹中時,binCount只賦值了2,并沒有計算整個樹中元素的個數(shù)
                // 所以不會重復(fù)樹化
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果要插入的元素已經(jīng)存在,則返回舊值
                if (oldVal != null)
                    return oldVal;
                // 退出外層大循環(huán),流程結(jié)束
                break;
            }
        }
        }
        // 成功插入元素,元素個數(shù)加1(是否要擴(kuò)容在這個里面)
        addCount(1L, binCount);
        // 成功插入元素返回null
        return null;
    }

整體流程跟HashMap比較類似,大致是以下幾步:

(1)如果桶數(shù)組未初始化,則初始化;

(2)如果待插入的元素所在的桶為空,則嘗試把此元素直接插入到桶的第一個位置;

(3)如果正在擴(kuò)容,則當(dāng)前線程一起加入到擴(kuò)容的過程中;

(4)如果待插入的元素所在的桶不為空且不在遷移元素,則鎖住這個桶(分段鎖);

(5)如果當(dāng)前桶中元素以鏈表方式存儲,則在鏈表中尋找該元素或者插入元素;

(6)如果當(dāng)前桶中元素以紅黑樹方式存儲,則在紅黑樹中尋找該元素或者插入元素;

(7)如果元素存在,則返回舊值;

(8)如果元素不存在,整個Map的元素個數(shù)加1,并檢查是否需要擴(kuò)容;

添加元素操作中使用的鎖主要有(自旋鎖 + CAS + synchronized + 分段鎖)。

為什么使用synchronized而不是ReentrantLock?

因為synchronized已經(jīng)得到了極大地優(yōu)化,在特定情況下并不比ReentrantLock差。


未完待續(xù)~~


歡迎關(guān)注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章, 與彤哥一起暢游源碼的海洋。

死磕 java集合之ConcurrentHashMap源碼分析(一)

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI