1. 你真的了解 ConcurrentHashMap 嗎?

        共 19772字,需瀏覽 40分鐘

         ·

        2021-07-03 11:49


        作者:Amaranth007

        blog.csdn.net/qq_32828253/article/details/110450249

        HashMap是線程不安,而hashTable雖然是線程安全,但是性能很差。java提供了ConcurrentHashMap來替代hashTable。

        我們先來看一下JDK1.7的currentMap的結(jié)構(gòu):

        在ConcurrentHashMap中有個(gè)重要的概念就是Segment。我們知道HashMap的結(jié)構(gòu)是數(shù)組+鏈表形式,從圖中我們可以看出其實(shí)每個(gè)segment就類似于一個(gè)HashMap。Segment包含一個(gè)HashEntry數(shù)組,數(shù)組中的每一個(gè)HashEntry既是一個(gè)鍵值對,也是一個(gè)鏈表的頭節(jié)點(diǎn)。

        在ConcurrentHashMap中有2的N次方個(gè)Segment,共同保存在一個(gè)名為segments的數(shù)組當(dāng)中。可以說,ConcurrentHashMap是一個(gè)二級哈希表。在一個(gè)總的哈希表下面,有若干個(gè)子哈希表。

        為什么說ConcurrentHashMap的性能要比HashTable好,HashTables是用全局同步鎖,而CconurrentHashMap采用的是鎖分段,每一個(gè)Segment就好比一個(gè)自治區(qū),讀寫操作高度自治,Segment之間互不干擾。

        先來看看幾個(gè)并發(fā)的場景:

        Case1:不同Segment的并發(fā)寫入

        不同Segment的寫入是可以并發(fā)執(zhí)行的。

        Case2:同一Segment的一寫一讀

        同一Segment的寫和讀是可以并發(fā)執(zhí)行的。

        Case3:同一Segment的并發(fā)寫入

        Segment的寫入是需要上鎖的,因此對同一Segment的并發(fā)寫入會被阻塞。

        由此可見,ConcurrentHashMap當(dāng)中每個(gè)Segment各自持有一把鎖。在保證線程安全的同時(shí)降低了鎖的粒度,讓并發(fā)操作效率更高。

        上面我們說過,每個(gè)Segment就好比一個(gè)HashMap,其實(shí)里面的操作原理都差不多,只是Segment里面加了鎖。

        Get方法:

        1. 為輸入的Key做Hash運(yùn)算,得到hash值。
        2. 通過hash值,定位到對應(yīng)的Segment對象
        3. 再次通過hash值,定位到Segment當(dāng)中數(shù)組的具體位置。
        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;
                if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
                    (tab = s.table) != null) {
                    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方法:

        1. 為輸入的Key做Hash運(yùn)算,得到hash值。
        2. 通過hash值,定位到對應(yīng)的Segment對象
        3. 獲取可重入鎖
        4. 再次通過hash值,定位到Segment當(dāng)中數(shù)組的具體位置。
        5. 插入或覆蓋HashEntry對象。
        6. 釋放鎖。
        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;
                }

        可以看出get方法并沒有加鎖,那怎么保證讀取的正確性呢,這是應(yīng)為value變量用了volatile來修飾,后面再詳細(xì)講解volatile。搜索公眾號Java知音,回復(fù)“2021”,送你一份Java面試題寶典)

        static final class HashEntry<K,V{
                final int hash;
                final K key;
                volatile V value;
                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;
                }
                .....

        size方法

        既然每一個(gè)Segment都各自加鎖,那么我們在統(tǒng)計(jì)size()的時(shí)候怎么保證解決一直性的問題,比如我們在計(jì)算size時(shí),有其他線程在添加或刪除元素。

        /**
             * Returns the number of key-value mappings in this map.  If the
             * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
             * <tt>Integer.MAX_VALUE</tt>.
             *
             * @return the number of key-value mappings in this map
             */

            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)
                            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;
            }

        ConcurrentHashMap的Size方法是一個(gè)嵌套循環(huán),大體邏輯如下:

        1. 遍歷所有的Segment。
        2. 把Segment的元素?cái)?shù)量累加起來。
        3. 把Segment的修改次數(shù)累加起來。
        4. 判斷所有Segment的總修改次數(shù)是否大于上一次的總修改次數(shù)。如果大于,說明統(tǒng)計(jì)過程中有修改,重新統(tǒng)計(jì),嘗試次數(shù)+1;如果不是。說明沒有修改,統(tǒng)計(jì)結(jié)束。
        5. 如果嘗試次數(shù)超過閾值,則對每一個(gè)Segment加鎖,再重新統(tǒng)計(jì)。
        6. 再次判斷所有Segment的總修改次數(shù)是否大于上一次的總修改次數(shù)。由于已經(jīng)加鎖,次數(shù)一定和上次相等。
        7. 釋放鎖,統(tǒng)計(jì)結(jié)束。

        可以看出JDK1.7的size計(jì)算方式有點(diǎn)像樂觀鎖和悲觀鎖的方式,為了盡量不鎖住所有Segment,首先樂觀地假設(shè)Size過程中不會有修改。當(dāng)嘗試一定次數(shù),才無奈轉(zhuǎn)為悲觀鎖,鎖住所有Segment保證強(qiáng)一致性。

        JDK1.7和JDK1.8中ConcurrentHashMap的區(qū)別

        1、在JDK1.8中ConcurrentHashMap的實(shí)現(xiàn)方式有了很大的改變,在JDK1.7中采用的是Segment + HashEntry,而Sement繼承了ReentrantLock,所以自帶鎖功能,而在JDK1.8中則取消了Segment,作者認(rèn)為Segment太過臃腫,采用Node + CAS + Synchronized

        ps:Synchronized一直以來被各種吐槽性能差,但java一直沒有放棄Synchronized,也一直在改進(jìn),既然作者在這里采用了Synchronized,可見Synchronized的性能應(yīng)該是有所提升的,當(dāng)然只是猜想哈哈哈。。。

        2、在上篇HashMap中我們知道,在JDK1.8中當(dāng)HashMap的鏈表個(gè)數(shù)超過8時(shí),會轉(zhuǎn)換為紅黑樹,在ConcurrentHashMap中也不例外。這也是新能的一個(gè)小小提升。

        3、在JDK1.8版本中,對于size的計(jì)算,在擴(kuò)容和addCount()時(shí)已經(jīng)在處理了。JDK1.7是在調(diào)用時(shí)才去計(jì)算。

        為了幫助統(tǒng)計(jì)size,ConcurrentHashMap提供了baseCount和counterCells兩個(gè)輔助變量和CounterCell輔助類,1.8中使用一個(gè)volatile類型的變量baseCount記錄元素的個(gè)數(shù),當(dāng)插入新數(shù)據(jù)或則刪除數(shù)據(jù)時(shí),會通過addCount()方法更新baseCount。

        //ConcurrentHashMap中元素個(gè)數(shù),但返回的不一定是當(dāng)前Map的真實(shí)元素個(gè)數(shù)?;贑AS無鎖更新
        private transient volatile long baseCount;

        private transient volatile CounterCell[] counterCells;  // 部分元素變化的個(gè)數(shù)保存在此數(shù)組中

        /**
             * {@inheritDoc}
             */

            public int size() {
                long n = sumCount();
                return ((n < 0L) ? 0 :
                        (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                        (int)n);
            }

        final long sumCount() {
                CounterCell[] as = counterCells; CounterCell a;
                long sum = baseCount;
                if (as != null) {
                    for (int i = 0; i < as.length; ++i) {
                        if ((a = as[i]) != null)
                            sum += a.value;
                    }
                }
                return sum;
            }

        END

        推薦資源

        歡迎添加程序汪個(gè)人微信 itwang007  進(jìn)粉絲群或圍觀朋友圈

        往期資源  需要請自取

        Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 03版

        臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!

        字節(jié)跳動總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放下載!

        堪稱神級的Spring Boot手冊,從基礎(chǔ)入門到實(shí)戰(zhàn)進(jìn)階

        臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開放下載!

        喜歡就"在看"唄^_^

        瀏覽 54
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 日韩女同一区二区三区 | 免费69成人无码无遮又大 | 大学生私密按摩hd | 日韩成人精品一区二区三区 | 壮汉♂野外强迫gay小说 |