Java併發—ConcurrentHashMap

Java併發容器—ConcurrentHashMap

1.JDK1.7版本

​ jdk1.7的實現結構圖如下所示

[外鏈圖片轉存失敗(img-Fy93gKVd-1566009849146)(/home/shidongxuan/.config/Typora/typora-user-images/1566002052129.png)]

CurrentHashMap是由Segment數組和HashEntry數組結構組成. Segment是一種可重入鎖(ReentrantLock), 在ConcurrentHashMap裏扮演鎖的角色, HashEntry用於存儲鍵值對數據. 綜上, ConcurrentHashMap採用了分段鎖技術, 其中Segment繼承與ReentrantLock. 不像HashTable那樣不管是put操作還是get操作都需要做同步處理, 理論上ConcurrentHashMap支持段級別的線程併發, 當一個線程佔用鎖訪問一個Segment時, 不會影響其他Segment

1>put方法

	//先定位到Segment, 之後在對應的Segment上進行具體的put
	public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
	//具體的put
	final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //先嚐試對segment加鎖, 如果加鎖成功, 那麼node=null,如果加鎖失敗則調用
        	//scanAndLockForPut方法去獲取鎖, 這個方法中, 獲取鎖後會返回對應的HashEntry 
        	HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                //優化點, 這時已經加了鎖,只會有一個線程訪問table, 但是table是volatile修飾的
                //所以將其賦值給一個局部變量, 減少volatile帶來的開銷
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                //獲取到對應位置上的HashEntry鏈表
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        //e不爲空, 說明出現了hash衝突, 遍歷當前鏈表
                        //如果找到一個key相同的, 則根據onlyIfAbsent判斷是否覆蓋value值
                        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 {
                        //說明e爲空, 一種可能是在上面遍歷到最後沒有找到key相同的, 也可能是一開始就是null
                        //第一次沒有獲取到鎖, 通過scanAndLockForPut方法獲取到了鎖,放到敵營位置上
                        if (node != null)
                            node.setNext(first);
                        else
                            //否則新new出一個節點對象
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;//節點數+1
                        //判斷是否需要擴容
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            //數組無須擴容, 就直接插入node到指定index位置
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

下面是獲取鎖的scanAndLockForPut方法

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    //如果嘗試加鎖失敗,那麼就對segment[hash]對應的鏈表進行遍歷找到需要put的這個entry所在的鏈表中的位置,
    //這裏之所以進行一次遍歷找到坑位,主要是爲了通過遍歷過程將遍歷過的entry全部放到CPU高速緩存中,
    //這樣在獲取到鎖了之後,再次進行定位的時候速度會十分快,這是在線程無法獲取到鎖前並等待的過程中的一種預熱方式。
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //獲取鎖失敗,初始時retries=-1必然開始先進入第一個if
        if (retries < 0) {//<1>
            if (e == null) { //<1.1>
                //e=null代表兩種意思,第一種就是遍歷鏈表到了最後,仍然沒有發現指定key的entry;
                //第二種情況是剛開始時確實太過entryForHash找到的HashEntry就是空的,即通過hash找到的table中對應位置鏈表爲空
                //當然這裏之所以還需要對node==null進行判斷,是因爲有可能在第一次給node賦值完畢後,然後預熱準備工作已經搞定,
                //然後進行循環嘗試獲取鎖,在循環次數還未達到<2>以前,某一次在條件<3>判斷時發現有其它線程對這個segment進行了修改,
                //那麼retries被重置爲-1,從而再一次進入到<1>條件內,此時如果再次遍歷到鏈表最後時,因爲上一次遍歷時已經給node賦值過了,
                //所以這裏判斷node是否爲空,從而避免第二次創建對象給node重複賦值。
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))//<1.2>   遍歷過程發現鏈表中找到了我們需要的key的坑位
                retries = 0;
            else//<1.3>   當前位置對應的key不是我們需要的,遍歷下一個
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {//<2>
            // 嘗試獲取鎖次數超過設置的最大值,直接進入阻塞等待,這就是所謂的有限制的自旋獲取鎖,
            //之所以這樣是因爲如果持有鎖的線程要過很久才釋放鎖,這期間如果一直無限制的自旋其實是對系統性能有消耗的,
            //這樣無限制的自旋是不利的,所以加入最大自旋次數,超過這個次數則進入阻塞狀態等待對方釋放鎖並獲取鎖。
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {//<3>
            // 遍歷過程中,有可能其它線程改變了遍歷的鏈表,這時就需要重新進行遍歷了。
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

其核心思想就是通過MAX_SCAN_RETRIES控制自旋次數, 防止無限制的自旋和浪費資源. 這個方法的作用就是遍歷獲取所然後進行數據插入

2>rehash方法

/**
 * Doubles size of table and repacks entries, also adding the
 * given node to new table
 * 對數組進行擴容,由於擴容過程需要將老的鏈表中的節點適用到新數組中,所以爲了優化效率,可以對已有鏈表進行遍歷,
 * 對於老的oldTable中的每個HashEntry,從頭結點開始遍歷,找到第一個後續所有節點在新table中index保持不變的節點fv,
 * 假設這個節點新的index爲newIndex,那麼直接newTable[newIndex]=fv,即可以直接將這個節點以及它後續的鏈表中內容全部直接複用copy到newTable中
 * 這樣最好的情況是所有oldTable中對應頭結點後跟隨的節點在newTable中的新的index均和頭結點一致,那麼就不需要創建新節點,直接複用即可。
 * 最壞情況當然就是所有節點的新的index全部發生了變化,那麼就全部需要重新依據k,v創建新對象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    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;
            if (next == null)   //  Single node on list 只有單個節點
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }//這個for循環就是找到第一個後續節點新的index不變的節點。
                newTable[lastIdx] = lastRun;
                // Clone remaining nodes
                //第一個後續節點新index不變節點前所有節點均需要重新創建分配。
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

3>ensureSegment方法

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]; // 以初始化時創建的第一個坑位的ss[0]作爲模版進行創建
        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) { // 二次檢查是否有其它線程創建了這個Segment
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                //這裏通過自旋的CAS方式對segments數組中偏移量爲u位置設置值爲s,這是一種不加鎖的方式,
                //萬一有多個線程同時執行這一步,那麼只會有一個成功,而其它線程在看到第一個執行成功的線程結果後
                //會獲取到最新的數據從而發現需要更新的坑位已經不爲空了,那麼就跳出while循環並返回最新的seg
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

這個方法核心是利用自旋CAS來創建對應的Segment, 這種思想是不加鎖保證線程安全.

4>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);//獲取key對應hash值
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//獲取對應h值存儲所在segments數組中內存偏移量
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //通過Unsafe中的getObjectVolatile方法進行volatile語義的讀,獲取到segments在偏移量爲u位置的分段Segment,
        //並且分段Segment中對應table數組不爲空
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {//獲取h對應這個分段中偏移量爲xxx下的HashEntry的鏈表頭結點,然後對鏈表進行 遍歷
            //###這裏第一次初始化通過getObjectVolatile獲取HashEntry時,獲取到的是主存中最新的數據,但是在後續遍歷過程中,有可能數據被其它線程修改
            //從而導致其實這裏最終返回的可能是過時的數據,所以這裏就是ConcurrentHashMap所謂的弱一致性的體現,containsKey方法也一樣
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

這裏這個方法是弱一致性的, 所以可能會返回過時的數據

5>size方法

public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // 是否溢出
    long sum;         // 存儲本次循環過程中計算得到的modCount的值
    long last = 0L;   // 存儲上一次遍歷過程中計算得到的modCount的和
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {//無限for循環,結束條件就是任意前後兩次遍歷過程中modcount值的和是一樣的,說明第二次遍歷沒有做任何變化
            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 {
        //由於只有在retries等於RETRIES_BEFORE_LOCK時纔會執行強制加鎖,並且由於是用的retries++,
        //所以強制加鎖完畢後,retries的值是一定會大於RETRIES_BEFORE_LOCK的,
        //這樣就防止正常遍歷而沒進行加鎖時進行鎖釋放的情況
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

如果要統計整個ConcurrentHashMap中的元素個數, 將每個Segment中的元素數量加起來就好了, 正好Segment中的count是一個volatile變量. 但如果在累加的時候, 其他線程進行了插入或刪除操作, 結果就不準了. 所以最安全的方法是對所有Segment的put, remove, clean方法鎖住, 但是這種方法很低效.

因爲在累加count的過程中, 之前累加過的count發生變化的機率很小, 所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment的大小, 如果統計過程中count發生了變化, 則再採用加鎖的方式統計所有Segment的大小

2.JDK1.8版本

在jdk1.8中, ConcurrentHashMap通過一個Node<K,V>[]數組來保存添加到map中的鍵值對, 在同一個數組位置是通過鏈表或紅黑樹的形式來表示, 其實原理類似於jdk1.8中的HashMap

1>put方法

	public V put(K key, V value) {
        return putVal(key, value, false);
    }
    /*
     * 當添加一對鍵值對的時候,首先會去判斷保存這些鍵值對的數組是不是初始化了,如果沒有的話就初始化數組
     *  然後通過計算hash值來確定放在數組的哪個位置
     * 如果這個位置爲空則直接添加,如果不爲空的話,則取出這個節點來
     * 如果取出來的節點的hash值是MOVED(-1)的話,則表示當前正在對這個數組進行擴容,複製到新的數組,則當前線程也去幫助複製
     * 最後一種情況就是,如果這個節點,不爲空,也不在擴容,則通過synchronized來加鎖,進行添加操作
     *    然後判斷當前取出的節點位置存放的是鏈表還是樹
     *    如果是鏈表的話,則遍歷整個鏈表,直到取出來的節點的key來個要放的key進行比較,如果key相等,並且key的hash值也相等的話,
     *          則說明是同一個key,則覆蓋掉value,否則的話則添加到鏈表的末尾
     *    如果是樹的話,則調用putTreeVal方法把這個元素添加到樹中去
     *  最後在添加完成之後,會判斷在該節點處共有多少個節點(注意是添加前的個數),如果達到8個以上了的話,
     *  則調用treeifyBin方法來嘗試將處的鏈表轉爲樹,或者擴容數組
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//K,V都不能爲空,否則的話跑出異常
        int hash = spread(key.hashCode());    //取得key的hash值
        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();    //第一次put的時候table沒有初始化,則初始化table
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {    /
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))        //創建一個Node添加到數組中區,null表示的是下一個節點爲空
                    break;                   // no lock when adding to empty bin
            }
            /*
             * 如果檢測到某個節點的hash值是MOVED,則表示正在進行數組擴張的數據複製階段,
             * 則當前線程也會參與去複製,通過允許多線程複製的功能,一次來減少數組的複製所帶來的性能損失
             */
            else if ((fh = f.hash) == MOVED)    
                tab = helpTransfer(tab, f);
            else {
                /*
                 * 如果在這個位置有元素的話,就採用synchronized的方式加鎖,
                 *     如果是鏈表的話(hash大於0),就對這個鏈表的所有元素進行遍歷,
                 *         如果找到了key和key的hash值都一樣的節點,則把它的值替換到
                 *         如果沒找到的話,則添加在鏈表的最後面
                 *  否則,是樹的話,則調用putTreeVal方法添加到樹中去
                 *  
                 *  在添加完之後,會對該節點上關聯的的數目進行判斷,
                 *  如果在8個以上的話,則會調用treeifyBin方法,來嘗試轉化爲樹,或者是擴容
                 */
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {   //再次取出要存儲的位置的元素,跟前面取出來的比較
                        if (fh >= 0) {  //取出來的元素的hash值大於0,當轉換爲樹之後,hash值爲-2
                            binCount = 1;            
                            for (Node<K,V> e = f;; ++binCount) {    //遍歷這個鏈表
                                K ek;
                                if (e.hash == hash &&        //要存的元素的hash,key跟要存儲的位置的節點的相同的時候,替換掉該節點的value即可
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)        //當使用putIfAbsent的時候,只有在這個key沒有設置值得時候才設置
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {    //如果不是同樣的hash,同樣的key的時候,則判斷該節點的下一個節點是否爲空,
                                    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,    //調用putTreeVal方法,將該元素添加到樹中去
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)    //當在同一個節點的數目達到8個的時候,則擴張數組或將給節點的數據轉爲tree
                        treeifyBin(tab, i);    
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);    //計數
        return null;
    }

2>擴容transfer方法

	private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  // 構建一個nextTable,大小爲table兩倍
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        //通過for自循環處理每個槽位中的鏈表元素,默認advace爲真,通過CAS設置transferIndex屬性值,並初始化i和bound值,i指當前處理的槽位序號,bound指需要處理的槽位邊界,先處理槽位15的節點;
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) { // 遍歷table中的每一個節點 
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {  // //如果所有的節點都已經完成複製工作  就把nextTable賦值給table 清空臨時對象nextTable  
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);  //擴容閾值設置爲原來容量的1.5倍  依然相當於現在容量的0.75倍
                    return;
                }
                // 利用CAS方法更新這個擴容閾值,在這裏面sizectl值減一,說明新加入一個線程參與到擴容操作
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //如果遍歷到的節點爲空 則放入ForwardingNode指針 
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //如果遍歷到ForwardingNode節點  說明這個點已經被處理過了 直接跳過  這裏是控制併發擴容的核心  
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {  
                        Node<K,V> ln, hn;
                        if (fh >= 0) {  // 鏈表節點
                            int runBit = fh & n;  // resize後的元素要麼在原地,要麼移動n位(n爲原capacity),詳解見:https://huanglei.rocks/coding/194.html#4%20resize()%E7%9A%84%E5%AE%9E%E7%8E%B0
                            Node<K,V> lastRun = f;
                            //以下的部分在完成的工作是構造兩個鏈表  一個是原鏈表  另一個是原鏈表的反序排列
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //在nextTable的i位置上插入一個鏈表 
                            setTabAt(nextTab, i, ln);
                            //在nextTable的i+n的位置上插入另一個鏈表
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            //設置advance爲true 返回到上面的while循環中 就可以執行i--操作

                            advance = true;
                        }
                        //對TreeBin對象進行處理  與上面的過程類似 
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            //構造正序和反序兩個鏈表 
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            // (1)如果lo鏈表的元素個數小於等於UNTREEIFY_THRESHOLD,默認爲6,則通過untreeify方法把樹節點鏈表轉化成普通節點鏈表;(2)否則判斷hi鏈表中的元素個數是否等於0:如果等於0,表示lo鏈表中包含了所有原始節點,則設置原始紅黑樹給ln,否則根據lo鏈表重新構造紅黑樹。
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd); // tab[i]已經處理完了
                            advance = true;
                        }
                    }
                }
            }
        }
    }

如何在擴容時,併發地複製與插入?

  1. 遍歷整個table,當前節點爲空,則採用CAS的方式在當前位置放入fwd
  2. 當前節點已經爲fwd(with hash field “MOVED”),則已經有有線程處理完了了,直接跳過 ,這裏是控制併發擴容的核心
  3. 當前節點爲鏈表節點或紅黑樹,重新計算鏈表節點的hash值,移動到nextTable相應的位置(構建了一個反序鏈表和順序鏈表,分別放置在i和i+n的位置上)。移動完成後,用Unsafe.putObjectVolatile在tab的原位置賦爲爲fwd, 表示當前節點已經完成擴容。

3>get方法

讀取操作,不需要同步控制,比較簡單

  1. 空tab,直接返回null
  2. 計算hash值,找到相應的bucket位置,爲node節點直接返回,否則返回null
ublic 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;
}

3.HashTable與ConcurrentHashMap對比

ConcurrentHashMap能完全替代HashTable嗎?
​ hash table雖然性能上不如ConcurrentHashMap,但並不能完全被取代,兩者的迭代器的一致性不同的, hash table的迭代器是強一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。

  • Hashtable的任何操作都會把整個表鎖住,是阻塞的。好處是總能獲取最實時的更新,比如說線程A調用putAll寫入大量數據,期間線程B調用get,線程B就會被阻塞,直到線程A完成putAll,因此線程B肯定能獲取到線程A寫入的完整數據。壞處是所有調用都要排隊,效率較低。
  • ConcurrentHashMap 是設計爲非阻塞的。在更新時會局部鎖住某部分數據,但不會把整個表都鎖住。同步讀取操作則是完全非阻塞的。好處是在保證合理的同步前提下,效率很高。壞處 是嚴格來說讀取操作不能保證反映最近的更新。例如線程A調用putAll寫入大量數據,期間線程B調用get,則只能get到目前爲止已經順利插入的部分數據。

選擇哪一個,是在性能與數據一致性之間權衡。ConcurrentHashMap適用於追求性能的場景,大多數線程都只做insert/delete操作,對讀取數據的一致性要求較低。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章