Java7與Java8中的HashMap和ConcurrentHashMap知識點總結


JAVA7中的ConcurrentHashMap簡介

Java7的ConcurrentHashMap里有多把鎖,每一把鎖用於其中一部分數據,那么當多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高並發訪問效率呢。這就是“鎖分離”技術。

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(繼承了ReentrantLock),在ConcurrentHashMap里扮演的角色,HashEntry則用於存儲鍵值對數據。

 

Java7中的ConcurrentHashMap底層邏輯結構

一個ConcurrentHashMap里包含一個Segment數組,Segment的結構和HashMap類似,是一種數組和鏈表結構,Segment數組每個Segment里包含一個HashEntry數組,一個HashEntry數組中的每個hashEntry對象是一個鏈表的頭結點每個鏈表結構中包含的元素才是Map集合中的key-value鍵值對。如下圖:

 

 

由上圖可見,ConcurrentHashMap的數據結構;一個Segment對應(鎖住)一個HashEntry數組。

每個Segment守護着一個HashEntry數組里的元素,當對HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖

確定插入的元素在哪一個Segment的位置,當然也是Hash,這個算法也是左移、右移等,然后再插入到具體的HashEntry數組中。

Java7中ConcurrentHashMap使用的就是鎖分段技術,ConcurrentHashMap由多個Segment組成(Segment下包含很多Node,也就是我們的鍵值對了),每個Segment都有把鎖來實現線程安全,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。

因此,關於ConcurrentHashMap就轉化為了對Segment的研究。這是因為,ConcurrentHashMap的get、put操作是直接委托Segmentget、put方法。

 

JAVA8中的ConcurrentHashMap

個人的理解

Java8中的鎖的粒度比Java7中更細了,Java8鎖住的一個某一個數組元素table[i](頭節點,該頭結點類型是鏈表頭結點或紅黑樹的頭結點),而Java7中segment鎖住的是一個HashEntry數組,相當於鎖住了多個數組元素;所以我感覺Java8中ConcurrentHashMap多線程環境下 put效率更高。

 

HashMap核心數據結構之一Node

先來看一下HashMap(先理解HashMap再看ConcurrentHashMap更容易)集合底層的數組Node[] table的某一個元素table[i](即某一個鏈表的頭結點),表示Map集合一些泛型對象構成的鏈表或者紅黑樹

 

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;    //用來定位數組索引位置
        final K key;
        V value;
        Node<K,V> next;   //鏈表的下一個node
 
        Node(int hash, K key, V value, Node<K,V> next) { ... }
        public final K getKey(){ ... }
        public final V getValue() { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
}

 

  

(重點)HashMap的基本方法原理

HashMap為什么鏈表長度大於8就要轉紅黑樹?

紅黑樹的插入、刪除和遍歷的最壞時間復雜度都是log(n),因此,意外的情況或者惡意使用下導致hashcode()方法的返回值很差時,只要Key具有可比性,性能的下降將會是"優雅"的。
但由於TreeNodes的空間占用是常規Nodes兩倍,所以只有桶中包含足夠多的元素以供使用時,我們才應該使用樹。那么為什么這個數字會是8呢?

官方文檔的一段描述:

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they 
become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used.
Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter
of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity.
Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:
0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006 more: less than 1 in ten million

理想情況下,在隨機哈希代碼下,桶中的節點頻率遵循泊松分布,文中給出了桶長度k的頻率表。
由頻率表可以看出,同一個Hash數組位置沖突達到8概率非常非常小(一億分之六)。所以作者應該是根據概率統計而選擇了8作為閥值,由此可見,這個選擇是非常嚴謹和科學的。

所以鏈表轉紅黑樹概率很低;但是一旦元素個數大於8鏈表的查詢刪除(刪除要先查詢到才能刪)效率綜合來看比不上紅黑樹的效率(本文末有對紅黑樹簡單介紹)。

 

Java8 HashMap的hash算法

如果key為null,hash值為0;如果不為null,直接將key的hashcode值hh無符號右移16位后的數進行按位異或^位運算 

 

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

ConcurrentHashMap的計算Hash值函數和HashMap不太一樣

 

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
......
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

  

 

(重點)Java8解決Java7中HashMap多線程情況下造成的遍歷元素死循環

Java7原有造成死鎖的關鍵原因點: 多線程同時擴容時, 執行到resize中的transfer方法重新散列table散列元素為從頭插入, 最終形成環形鏈表,

調用get方法或put方法遍歷這個Hash數組的位置的鏈表,如果元素key不存在,就在這個循環鏈表中無限循環遍歷了,因為不存在尾結點的指針域為null停止循環遍歷了.

 

Java7 HashMap put元素后超過了閾值,就需要擴容,此時如果有多個線程同時擴容;

多線程擴容時, 都執行到resize調用transfer,會倒排原有的一個鏈表中元素的順序,因為是頭插法;

比如原來的順序是A->B->C,一個線程擴容后就可能是A,B->A,C->B->A,而另一個線程來擴容也是頭插法為C,B->C,A->B->C;

第一個線程有C->B第二個線程B->C,而這里形成了環形鏈表,即其中一個T1線程掛起,而另外的線程T2完成散列元素后,然后T1線程不再掛起,繼續散列元素,會形成一個環形鏈表;

下一次調用get方法或put方法遍歷這個Hash數組的位置的鏈表,如果元素key不存在,就在這個循環鏈表中無限循環遍歷了,因為不存在尾結點的指針域為null停止循環遍歷了。

 

Java8中的HashMap put時如果有沖突需要插入到鏈表尾部,Java7是插入到頭部;

不管是頭插入還是尾部插入,都是需要遍歷對應數組位置的所有鏈表或紅黑樹節點,因為如果key存在,是更新操作,返回oldValue;

Java8,除了對hashmap增加紅黑樹結果外,改進為依次在尾部添加新的元素;

Java8利用紅黑樹改進了鏈表過長查詢遍歷慢問題,多線程resize時保持原來鏈表的元素順序(  loTail、hiTail尾部插入不再根據key的hash值重新散列到新數組,

而是放到原數組下標或下標為(原數組下標+oldCapacity老數組容量)的位置,見Java8 HashMap resize源碼),避免出現導致put產生循環鏈表的bug。

 

 

Java8HashMap resize部分源碼

(重點)下面代碼中的"if ((e.hash & oldCap) == 0) "這里按位於位運算,實際上是判斷元素的key的hash值與原來老數組的容量的關系來決定擴容后元素放在新數組的下標;

比如老容量為16,二進制表示10000,如果hash值表示的二進制數從右往左數在第5位為1,則e.hash & oldCap 必定為10000不為0,而如果hash值表示的二進制數從右往左數在第5位為0,則則e.hash & oldCap 必定為0

比如老數組容量為16,A元素的hash值是17,那在原數組中A的下標為j = 17 & (16 - 1) = 1, 擴容后如果是正常散列17應該放在 17 & (32 -1)= 17的位置,而e.hash & oldCap 即17 & 16等於16不等於0,所以擴容后的位置為j+oldCap=17(j =1, oldCap = 16);

比如老數據容量為16,A元素的hash值是33,那在原數組中A的下標為j = 33 & (16 - 1) = 1, 擴容后如果是正常散列33應該放在 33 & (32 -1)= 1的位置,而e.hash & oldCap 即33 & 16等於0,所以擴容后的位置仍然是為j(j =1);

可以簡單體會下擴容位運算判斷元素散列到原來老數組這一半的下標,還是新數組這一半的下標,與元素正常散列到hash新數組的關系。

final Node<K,V>[] resize() {
       省略部分代碼...
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
     // (重點)可以簡單體會下擴容位運算判斷元素散列到原來老數組這一半的下標,還是新數組這一半的下標,與正常散列到hash新數組的關系。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }                

 

JDK1.8的ConcurrentHashMap中Segment

JDK1.8的ConcurrentHashMap中Segment雖保留,但已經簡化屬性,僅僅是為了兼容舊版本

/**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

 

JAVA8 ConcurrentHashMap與HashMap的一些相通之處

JAVA8中ConcurrentHashMap的底層與Java8的HashMap有相通之處,底層依然由“數組”+鏈表+紅黑樹來實現的,底層結構存放的是TreeBin對象,而不是TreeNode對象;TreeBin包裝的很多TreeNode節點,它代替了TreeNode的根節點,也就是說在實際的ConcurrentHashMap“數組”中,存放的是TreeBin對象,而不是TreeNode對象,這是與HashMap的區別之一。

 

ConcurrentHashMap使用CAS更新value

ConcurrentHashMap實現中借用了較多的CAS(Compare And Swap)算法(sun.misc.Unsafe對象中有很多native本地方法,如unsafe.compareAndSwapInt(this, valueOffset, expect, update))。

意思是如果valueOffset位置包含的值與expect相同,則更新valueOffset位置的值為update,並返回true,否則不更新,返回false。

我理解為如果將要更新的變量的值如果和這個線程從Map中取出值相同,那么就更新,否則就不更新(和CAS的思想一樣)。

 

ConcurrentHashMap使用了synchronized關鍵字進行同步

ConcurrentHashMap既然借助了CAS來實現非阻塞的無鎖實現更改value線程安全,那么是不是就沒有用鎖了呢??答案:還是使用了synchronized關鍵字進行同步了的,在哪里使用了呢?在操作同一Node數組下標的鏈表或紅黑樹頭結點還是會synchronized上鎖,這樣才能保證線程安全。

 

(重點)為什么需要synchronized關鍵字進行同步?

 

因為如果不鎖住該位置的頭結點,當一個線程在對該Hash數組該位置的鏈表或者紅黑樹進行操作時,如果其他線程操作(修改,添加元素,刪除等)引起了Map的resize(擴容或縮減),該鏈表或紅黑樹hash值可能會發生變化,而正在進行寫操作(如put)的線程會因為hash值改變而找不到該位置對於的元素,還有例如插入到當前尾結點后面,如果當前尾結點正好被刪除了就會有問題

鎖住了頭結點,其他線程就無法操作(修改,添加元素,刪除等)該鏈表或者紅黑樹,當持有鎖的線程操作完畢,釋放頭結點鎖,其他線程才有機會獲得該位置的鎖,然后進行操作。

 

ConcurrentHashMap整個類的源碼,和HashMap的實現基本一模一樣,當有修改操作時借助了synchronized來對table[i](頭結點)進行鎖定保證了線程安全以及使用了CAS來保證原子性操作,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的實現思路為:根據key的hash值找到其在table所對應的位置i,然后在table[i]位置所存儲的鏈表(或者是樹)進行查找是否有鍵為key的節點,如果有,則返回節點對應的value,否則返回null。

 

(重點)為什么要指定HashMap的容量?

首先創建HashMap指定容量比如1024后,並不是HashMap的size不是1024,而是0,插入多少元素,size就是多少;

然后如果不指定HashMap的容量,要插入768個元素,第一次容量為16,需要持續擴容多次到1024,才能保存1024*0.75=768個元素;

HashMap擴容是非常非常消耗性能的,Java7中每次先創建2倍當前容量的新數組,然后再將老數組中的所有元素次通過hash&(length-1)的方式散列到HashMap中;

Java8中HashMap雖然不需要重新根據hash值散列后的新數組下標,但是由於需要遍歷每個Node數組的鏈表或紅黑樹中的每個元素,根據元素key的hash值原來老數組的容量的關系來決定放到新Node數組哪一半(2倍擴容),還是需要時間的。

 

(重點)HashMap指定容量初始化后,底層Hash數組已經被分配內存了嗎?

Java8 HashMap指定容量構造方法源碼,會調整threshold2的整數次冪,比如指定容量為1000,則threshold為1024

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
 }

/**
     * Returns a power of two size for the given target capacity.
     */
 static final int tableSizeFor(int cap) {
        int n = cap - 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;
    }

可以根據源碼看到,在指定的HashMap初始容量后,底層的Hash數組Node<K,V>[] table此時並沒有被初始化,依舊為null;

那么是什么時候被初始化的呢?見:https://www.cnblogs.com/theRhyme/p/11763685.html

 

 

ConcurrentHashMap類中核心屬性的介紹

 

sizeCtl最重要的屬性之一,看源碼之前,這個屬性表示什么意思,一定要記住。

private transient volatile int sizeCtl;//控制標識符

 

sizeCtl是控制標識符,不同的值表示不同的意義。

 

  • 負數代表正在進行初始化擴容操作 ,其中-1代表正在初始化 ;-N(N>1) 表示有N-1個線程正在進行擴容操作
  • 0表示未被初始化
  • 正數表示下一次進行擴容的大小,sizeCtl的值 = 當前ConcurrentHashMap容量*0.75,這與loadfactor是對應的。實際容量>=sizeCtl,則擴容。

 

 

1、 transient volatile Node<K,V>[] table;

 

是一個Node數組,第一次插入數據的時候初始化,大小是2的冪次方。這就是我們所說的底層結構:”數組+鏈表(或樹)”。

注意這里的Node數組加了volatile(volatile關鍵字的作用進行修飾,table數組在內存中對所有線程都及時可見,如果一個線程修改了table數組的值,其他線程中如果自己的線程棧中有table的副本,就會把table緩存行設置為失效,強制從內存中讀取table數組的值。

所以一個線程調用ConcurrentHashMap的get方法也能獲得最新的Map集合元素的值(迭代器可能是舊值)。

 

 

2、private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量

3、private static final intDEFAULT_CAPACITY = 16;

4、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647

5、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;

6、private static final float LOAD_FACTOR = 0.75f;

7、static final int TREEIFY_THRESHOLD = 8; // 鏈表轉樹的閾值,如果table[i]下面的鏈表長度大於8時就轉化為數

8、static final int UNTREEIFY_THRESHOLD = 6; //樹轉鏈表的閾值,小於等於6是轉為鏈表,僅在擴容tranfer時才可能樹轉鏈表

9、static final int MIN_TREEIFY_CAPACITY = 64;

10、private static final int MIN_TRANSFER_STRIDE = 16;

11、private static int RESIZE_STAMP_BITS = 16;

12、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大線程數

13、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

14、static final int MOVED = -1; // hash for forwarding nodes(節點在Map中的hash值)、標示位

15、static final int TREEBIN = -2; // hash for roots of trees(樹根節點的hash值

16、static final int RESERVED = -3; // hash for transient reservations(保留節點的hash值,為了擴容的時候吧)

 

在ConcurrentHashMap中有一個構造方法中有一個參數:concurrencyLevel,表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大線程數默認值為16,(即允許16個線程並發可能不會產生競爭)。為了保證並發的性能,我們要很好的估計出concurrencyLevel值,不然要么競爭相當厲害,從而導致線程試圖寫入當前鎖定的段時阻塞。

 

ConcurrentHashMap的get()方法沒有對任何對象加鎖,所以可能會讀到的數據(比如通過迭代器遍歷的時候,其他線程修改了數組元素),和HashMap的get方法的原理是一模一樣的。 

 

ConcurrentHashMap鏈表轉樹時,並不會直接轉,正如注釋(Nodes for use in TreeBins)所說,只是把這些節點包裝成TreeNode放到TreeBin中,再由TreeBin來轉化紅黑樹

 

ConcurrentHashMap與HashMap的一些區別

ConcurrentHashMap不允許null key或value,HashMap可以;

ConcurrentHashMap的計算Hash值函數也和HashMap不一樣:

還有一些如前所說的一些和其他區別,就不打算詳細說明了。

 

(重點)ConcurrentHashMap的putVal方法

putVal(K key, V value, boolean onlyIfAbsent, boolean evict)大體流程如下:
1、檢查key/value是否為null,如果為null,則拋異常,否則進行2
2、進入for死循環,進行3
3、檢查table是否初始化了,如果沒有,則調用initTable()進行初始化然后進行 2,否則進行4
4、根據key的hash值計算出其應該在table中儲存的位置i(根據key的hashcode計算出Hash值,在將Hash值與length-1進行按位與,length是2的整數次冪,減1后的二進制與Hash值進行按位與相當於取余運算,但取余的位運算次數肯定不止1次,而這里一次位運算就得出結果效率更高),取出table[i]的節點用f表示。
根據f的不同有如下三種情況:

  1)如果table[i]==null(即該位置的節點為空,沒有發生碰撞),則利用CAS操作直接存儲在該位置,如果CAS操作成功則退出死循環
  2)如果table[i]!=null(即該位置已經有其它節點,發生碰撞),碰撞處理也有兩種情況
    2.1)檢查table[i]的節點的hash是否等於MOVED(-1),如果等於,則檢測到正在擴容,則幫助其擴容
    2.2)說明table[i]的節點的hash值不等於MOVED,synchronized鎖住頭結點table[i],進行插入操作;

        如果table[i]為鏈表節點,則將此節點插入鏈表末尾中即可;

        如果table[i]為樹節點,則將此節點插入樹中即可;

        插入成功后,進行 5

5、如果table[i]的節點是鏈表節點,則檢查table的第i個位置的鏈表的元素個數是否大於了8,大於8就需要轉化為,如果需要則調用treeifyBin函數進行轉化。
  鏈表轉樹:將數組tab的第index位置的鏈表轉化為紅黑樹。
6、插入成功后,如果key已經存在,返回oldValue;key開始不存在,返回null

 

上面第4點中的2)中的2.1)幫助擴容:如果當前正在擴容,則嘗試協助其擴容死循環再次發揮了重試的作用,有趣的是ConcurrentHashMap是可以多線程同時擴容的。

這里說協助的原因在於,對於數組擴容,一般分為兩步:1.新建一個更大的數組;2.將原數組數據(重新散列Hash值)copy到新數組中

對於第一步,ConcurrentHashMap通過CAS來控制一個int變量保證新建數組這一步建一個更大的數組只會執行一次;

對於第二步,ConcurrentHashMap采用CAS + synchronized + 移動后標記 的方式來達到多線程擴容的目的。

感興趣可以查看transfer函數。 

目前的猜想多線程擴容可能是多線程操作不同的table位置的鏈表或紅黑樹,檢查table[i]的節點的hash是否等於MOVED(-1),如果是幫助其擴容,將元素重新散列新的table數組的對應位置中。

 

ConcurrentHashMap實現高效的並發操作的3個函數(與sun.misc.Unsafe U對象有關)

這得益於ConcurrentHashMap中的如下三個函數(sun.misc.Unsafe U)

/* 3個用的比較多的CAS操作 */ @SuppressWarnings("unchecked") // ASHIFT等均為private static final 
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // 獲取索引i處Node 
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } // 利用CAS算法設置i位置上的Node節點(將ctable[i]比較,相同則插入v)。 
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } // 在鏈表轉樹時用到了這個方法;設置節點位置的值,僅在上鎖區被調用 
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);  
    }

 

 

 

private final void treeifyBin(Node<K,V>[] tab, int index) { …… } treeifyBin方法的思想:檢查下table的長度Map的容量,不是乘以加載因子的那個,也不是目前集合中元素的個數)是否大於等於MIN_TREEIFY_CAPACITY(64),如果不大於,
則調用tryPresize方法將table兩倍擴容就可以了,就不降鏈表轉化為樹了;如果大於,則就將table[i]鏈表轉化為樹

 

 

public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; // ignore transient negative values
} 這是Java8中查詢元素個數的方法mappingCount,返回值是long;這個應該用來代替size()方法被使用。這是因為ConcurrentHashMap可能比包含更多的映射結果,即超過int類型的
最大值(size()方法的返回值是int);這個方法返回值是一個估計值,由於存在並發的插入刪除,因此返回值可能與實際值會有出入。

 

 

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; }
元素個數

HashMap中的迭代器為什么會拋出 ConcurrentModificationException異常(源碼解析)?

HashMap不是線程安全的,其中的原因之一就是:在多線程環境下,一個線程通過迭代器遍歷或刪除時,另一個線程修改了HashMap,則modCount++,造成迭代器中的expectedModCount與HashMap中的modCount不一樣。

HashMap中迭代器源碼如下:

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

 

ConcurrentHashMap 中的迭代器(弱一致)

遍歷過程中,如果已經遍歷的數組上的內容變化了,迭代器不會拋出 ConcurrentModificationException 異常。

如果未遍歷的數組上的內容發生了變化,則有可能反映到迭代過程中。

這就是 ConcurrentHashMap 迭代器弱一致的表現。

在這種迭代方式中,當 iterator 被創建后,集合再發生改變就不再是拋出 ConcurrentModificationException,取而代之的是在改變時 new 新的數據 從而不影響原有的數據,iterator 完成后再將頭指針替換為新的數據,這樣 iterator 線程可以使用原來老的數據,而寫線程也可以並發的完成改變,更重要的,這保證了多個線程並發執行的連續性和擴展性,是性能提升的關鍵。 總結,ConcurrentHashMap 的弱一致性主要是為了提升效率,是一致性 與效率之間的一種權衡。

 

紅黑樹簡單介紹


算法導論中lgn默認都是以2為底的,即為log2n


紅黑樹使二叉搜索樹更加平衡:紅黑樹是一種二叉搜索樹,但在每個節點上增加一個存儲位表示節點的顏色,可是 red 或 black,紅黑樹的查找、插入、刪除的時間復雜度最壞為 O(lgn)

 

 

(重點)樹的高度決定查詢性能,讓樹盡可能平衡,就是為了降低樹的高度

 

 

因為由 n 個節點隨機生成的二叉搜索樹高度 lgn,所以二叉搜索樹的一般操作的執行時間為 O(lgn)


紅黑樹是犧牲了嚴格的高度平衡的優越條件為代價,紅黑樹能夠以O(log2 n)的時間復雜度進行搜索、插入、刪除操作。

此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。


紅黑樹的查詢性能略微遜色於AVL(平衡二叉查找樹)樹,因為他比avl樹會稍微不平衡最多一層,也就是說紅黑樹的查詢性能只比相同內容的avl樹最多多一次比較

但是,紅黑樹在插入和刪除上完勝avl樹,avl樹每次插入刪除會進行大量的平衡度計算,而紅黑樹為了維持紅黑性質所做的紅黑變換和旋轉的開銷,相較於avl樹為了維持平衡的開銷要小得多


紅黑樹的特性大致有三個(換句話說,插入、刪除節點后整個紅黑樹也必須滿足下面的三個性質,如果不滿足則必須進行旋轉):
1.根節點葉節點都是黑色節點,其中葉節點為Null節點
2.每個紅色節點的兩個子節點都是黑色節點,換句話說就是不能有連續兩個紅色節點,但是黑色節點可以連續
3.從節點到所有葉子節點上的黑色節點數量是相同

上述的性質約束了紅黑樹的關鍵:從根到葉子的最長可能路徑不多於最短可能路徑的兩倍長。

得到這個結論的理由是:
紅黑樹中最短的可能路徑是全部為黑色節點的路徑;
紅黑樹中最長的可能路徑是紅黑相間的路徑。

 

文件系統和數據庫的索引為什么用的是B+樹而不是紅黑樹或B樹

https://www.toutiao.com/a6739784316991046155/?timestamp=1569399352&app=news_article&group_id=6739784316991046155&req_id=2019092516155101002607708223015B85

B樹與紅黑樹比較:

內存中,紅黑樹效率更高,但是涉及到磁盤操作B樹更優;

文件系統和數據庫的索引都是存在硬盤上的,並且如果數據量大的話,不一定能一次性加載到內存中,B樹可以根據索引每次加載一個節點,再繼續往下找;

B+樹的多路存儲非葉結點保存的索引,而紅黑樹所有節點保存的是結果值;

紅黑樹是二叉的,保存大量記錄樹高度太高了,查詢效率不高。

 

B+樹與B樹比較

B 不管葉子節點還是非葉子節點,都會保存數據,這樣導致在非葉子節點中能保存的指針數量變少

B樹指針少的情況下要保存大量數據,只能增加樹的高度,導致 IO 操作變多,查詢性能變低;

樹的高度決定查詢性能,就是為了降低樹的高度

范圍查詢B+樹葉子節點保存了指向下一個葉子節點的指針,B+樹所有葉結點構成一個有序鏈表,在查詢中范圍查詢很常見,B樹范圍查詢效率低;

遍歷B+樹葉子節點就能獲得所有的記錄,相當於整棵樹的記錄遍歷

 

 

二叉樹、平衡樹、紅黑樹 

平衡樹是為了解決二叉查找樹退化為鏈表的情況,而紅黑樹是為了解決平衡樹在插入、刪除等操作需要頻繁調整

https://mp.weixin.qq.com/s/t1J2HnAhzrks-6AD4s8JYw

TODO待寫

 

鏈表轉紅黑樹、紅黑樹的插入與刪除等沒有詳細說明。

ConcurrentHashMap中紅黑樹相關:https://blog.csdn.net/chenssy/article/details/73749297

紅黑樹30張圖詳解!!!

 

來源:

https://blog.csdn.net/u010412719/article/details/52145145

http://www.importnew.com/20386.html

淺析幾種線程安全模型-importNew

 https://blog.csdn.net/daiyuhe/article/details/89424736

https://blog.csdn.net/linsongbin1/article/details/54708694


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM