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

JAVA7中的ConcurrentHashMap簡介

Java7的ConcurrentHashMap裏有多把鎖,每一把鎖用於其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效的提升併發訪問效率呢。這就是「鎖分離」技術。html

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

 

Java7中的ConcurrentHashMap底層邏輯結構

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

 

 

由上圖可見,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 HashMap put元素後超過了閾值,就須要擴容,此時若是有多線程同時擴容(resize調用transfer,會倒排原有的一個鏈表中元素的順序,由於是頭插法,好比原來的順序是A->B->C,擴容後就多是A,B->A,C->B->A,而另外一個線程來擴容也是頭插法爲C,B->C,A->B->C,第一個線程有C->B第二個線程B->C,而這裏造成了環形鏈表)狀況下,其中一個A線程掛起,而另外的線程完成擴容後,而後A線程再也不掛起,繼續擴容,會造成一個環形鏈表,下一次調用get方法或put方法遍歷這個Hash數組的位置的鏈表,若是元素key不存在,就在這個循環鏈表中無限循環遍歷了,由於不存在尾結點的指針域爲null中止循環遍歷了。

Java8中的HashMap put時若是有衝突須要插入到鏈表尾部,Java7是插入到頭部;

無論是頭插入仍是尾部插入,都是須要遍歷對應數組位置的全部鏈表或紅黑樹節點,由於若是key存在,是更新操做,返回oldValue;

Java8,除了對hashmap增長紅黑樹結果外,對Java7原有形成死鎖的關鍵緣由點(擴容時新table複製在頭端添加元素)改進爲依次在末端添加新的元素;

Java8利用紅黑樹改進了鏈表過長查詢遍歷慢問題,多線程resize時保持原來鏈表的元素順序(  loTail、hiTail尾部插入再也不根據key的hash值從新散列到新數組,而是放到原數組下標或下標爲(原數組下標+oldCapacity老數組容量)的位置,見Java8 HashMap resize源碼),避免出現致使put產生循環鏈表的bug。

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

 

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,則擴容。

 

 

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

 

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

注意這裏的Node數組加了volatile(volatile關鍵字的做用進行修飾,table數組在內存中對全部線程都及時可見,若是一個線程修改了table數組的值,其餘線程中若是本身的線程棧中有table的副本,就會把table緩存行設置爲失效,強制從內存中讀取table數組的值。

因此一個線程調用ConcurrentHashMap的get方法也能得到最新的Map集合元素的值(迭代器多是舊值)。

 

 

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

三、private static final intDEFAULT_CAPACITY = 16;

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

五、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;

六、private static final float LOAD_FACTOR = 0.75f;

七、static final int TREEIFY_THRESHOLD = 8; // 鏈表轉樹的閾值,若是table[i]下面的鏈表長度大於8時就轉化爲數

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

九、static final int MIN_TREEIFY_CAPACITY = 64;

十、private static final int MIN_TRANSFER_STRIDE = 16;

十一、private static int RESIZE_STAMP_BITS = 16;

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

1三、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

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

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

1六、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)大致流程以下:
一、檢查key/value是否爲null,若是爲null,則拋異常,不然進行2
二、進入for死循環,進行3
三、檢查table是否初始化了,若是沒有,則調用initTable()進行初始化而後進行 2,不然進行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

五、若是table[i]的節點是鏈表節點,則檢查table的第i個位置的鏈表的元素個數是否大於了8,大於8就須要轉化爲,若是須要則調用treeifyBin函數進行轉化。
  鏈表轉樹:將數組tab的第index位置的鏈表轉化爲紅黑樹。
六、插入成功後,若是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張圖詳解!!!

 

來源:

http://www.javashuo.com/article/p-mtzfapmv-a.html

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

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

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

相關文章
相關標籤/搜索