<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官為什么喜歡問 HashMap 和 ConcurrentHashMap ?

          共 16155字,需瀏覽 33分鐘

           ·

          2021-06-08 06:26

          點(diǎn)擊“開發(fā)者技術(shù)前線”,選擇“星標(biāo)”

          讓一部分開發(fā)者看到未來

          作者:淺藍(lán)色的麻吉
          https://www.jianshu.com/p/a7767e6ff2a2
          一、什么是哈希表

          在討論哈希表之前,我們先大概了解下其他數(shù)據(jù)結(jié)構(gòu)在新增,查找等基礎(chǔ)操作執(zhí)行性能

          數(shù)組


          采用一段連續(xù)的存儲(chǔ)單元來存儲(chǔ)數(shù)據(jù)。對(duì)于指定下標(biāo)的查找,時(shí)間復(fù)雜度為O(1);

          通過給定值進(jìn)行查找,需要遍歷數(shù)組,逐一比對(duì)給定關(guān)鍵字和數(shù)組元素,時(shí)間復(fù)雜度為O(n),當(dāng)然,對(duì)于有序數(shù)組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復(fù)雜度提高為O(logn);

          對(duì)于一般的插入刪除操作,涉及到數(shù)組元素的移動(dòng),其平均復(fù)雜度也為O(n)


          線性鏈表


          對(duì)于鏈表的新增,刪除等操作(在找到指定操作位置后),僅需處理結(jié)點(diǎn)間的引用即可,時(shí)間復(fù)雜度為O(1),而查找操作需要遍歷鏈表逐一進(jìn)行比對(duì),復(fù)雜度為O(n)


          二叉樹


          對(duì)一棵相對(duì)平衡的有序二叉樹,對(duì)其進(jìn)行插入,查找,刪除等操作,平均復(fù)雜度均為O(logn)。


          數(shù)組


          相比上述幾種數(shù)據(jù)結(jié)構(gòu),在哈希表中進(jìn)行添加,刪除,查找等操作,性能十分之高,不考慮哈希沖突的情況下,僅需一次定位即可完成,時(shí)間復(fù)雜度為O(1).

          哈希表上面的特性,哈希表的主干就是數(shù)組。

          比如我們要新增或查找某個(gè)元素,我們通過把當(dāng)前元素的關(guān)鍵字 通過某個(gè)函數(shù)映射到數(shù)組中的某個(gè)位置,通過數(shù)組下標(biāo)一次定位就可完成操作。

          存儲(chǔ)位置 = f(關(guān)鍵字)

          其中,這個(gè)函數(shù)f一般稱為哈希函數(shù),這個(gè)函數(shù)的設(shè)計(jì)好壞會(huì)直接影響到哈希表的優(yōu)劣。查找操作同理,先通過哈希函數(shù)計(jì)算出實(shí)際存儲(chǔ)地址,然后從數(shù)組中對(duì)應(yīng)地址取出即可。
          二、哈希沖

          通過哈希函數(shù)得出的實(shí)際存儲(chǔ)地址相同怎么辦?也就是說,當(dāng)我們對(duì)某個(gè)元素進(jìn)行哈希運(yùn)算,得到一個(gè)存儲(chǔ)地址,然后要進(jìn)行插入的時(shí)候,發(fā)現(xiàn)已經(jīng)被其他元素占用了,其實(shí)這就是所謂的哈希沖突,也叫哈希碰撞。
          哈希函數(shù)的設(shè)計(jì)至關(guān)重要,好的哈希函數(shù)會(huì)盡可能地保證 計(jì)算簡單和散列地址分布均勻,但是不可能設(shè)計(jì)出一個(gè)絕對(duì)完美的哈希函數(shù),我們需要清楚的是,數(shù)組是一塊連續(xù)的固定長度的內(nèi)存空間,再好的哈希函數(shù)也不能保證得到的存儲(chǔ)地址絕對(duì)不發(fā)生沖突。
          哈希沖突的解決方案有多種:開放定址法(發(fā)生沖突,繼續(xù)尋找下一塊未被占用的存儲(chǔ)地址),再散列函數(shù)法,鏈地址法,HashMap即是采用了鏈地址法.
          • JDK7 使用了數(shù)組+鏈表的方式

          • JDK8 使用了數(shù)組+鏈表+紅黑樹的方式



          三、HashMap的實(shí)現(xiàn)原理


          HashMap的主干是一個(gè)Entry數(shù)組。Entry是HashMap的基本組成單元,每一個(gè)Entry包含一個(gè)key-value鍵值對(duì)。

          transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
          Entry是HashMap中的一個(gè)靜態(tài)內(nèi)部類。
          static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存儲(chǔ)指向下一個(gè)Entry的引用,單鏈表結(jié)構(gòu) int hash;//對(duì)key的hashcode值進(jìn)行hash運(yùn)算后得到的值,存儲(chǔ)在Entry,避免重復(fù)計(jì)算
          /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }

          HashMap的整體結(jié)構(gòu)如下:

          • 解決沖突的鏈表的長度影響到HashMap查詢的效率

            簡單來說,HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數(shù)組位置不含鏈表(當(dāng)前entry的next指向null),那么對(duì)于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表,對(duì)于添加操作,其時(shí)間復(fù)雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對(duì)于查找操作來講,仍需遍歷鏈表,然后通過key對(duì)象的equals方法逐一比對(duì)查找。所以,性能考慮,HashMap中的鏈表出現(xiàn)越少,性能才會(huì)越好。

          • 發(fā)生沖突關(guān)于entry節(jié)點(diǎn)插入鏈表還是鏈頭呢?

            JDK7:插入鏈表的頭部,頭插法
            JDK8:插入鏈表的尾部,尾插法


          JDK7

          public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } }
          modCount++; addEntry(hash, key, value, i); return null; }

          閱讀源碼發(fā)現(xiàn),如果遍歷鏈表都沒法發(fā)現(xiàn)相應(yīng)的key值的話,則會(huì)調(diào)用addEntry方法在鏈表添加一個(gè)Entry,重點(diǎn)就在與addEntry方法是如何插入鏈表的,addEntry方法源碼如下:

          voidaddEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length);}

          這里構(gòu)造了一個(gè)新的Entry對(duì)象(構(gòu)造方法的最后一個(gè)參數(shù)傳入了當(dāng)前的Entry鏈表),然后直接用這個(gè)新的Entry對(duì)象取代了舊的Entry鏈表,看一下Entry的構(gòu)造方法可以知道是頭插法。

          Entry( int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h;}

          從構(gòu)造方法中的next=n可以看出確實(shí)是把原本的鏈表直接鏈在了新建的Entry對(duì)象的后邊,可以斷定是插入頭部。

          JDK8


          還是繼續(xù)查看put方法的源碼查看插入節(jié)點(diǎn)的代碼:
          //e是p的下一個(gè)節(jié)點(diǎn)if ((e = p.next) == null) { //插入鏈表的尾部 p.next = newNode(hash, key, value, null); //如果插入后鏈表長度大于8則轉(zhuǎn)化為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break;}
          從這段代碼中可以很顯然地看出當(dāng)?shù)竭_(dá)鏈表尾部(即p是鏈表的最后一個(gè)節(jié)點(diǎn))時(shí),e被賦為null,會(huì)進(jìn)入這個(gè)分支代碼,然后會(huì)用newNode方法建立一個(gè)新的節(jié)點(diǎn)插入尾部。


          四、HashMap的默認(rèn)參數(shù)理解

          1.為什么HashMap的Entry數(shù)組長度默認(rèn)為16呢?為什么數(shù)組長度一定要是2的n次冪呢?

          查看HashMap計(jì)算hashcode的方法獲取存儲(chǔ)的位置:

          為了減少hash值的碰撞,需要實(shí)現(xiàn)一個(gè)盡量均勻分布的hash函數(shù),在HashMap中通過利用key的hashcode值,來進(jìn)行位運(yùn)算。


          存儲(chǔ)的流程
          /** * Returns index for hash code h. */static int indexFor(int h, int length) { return h & (length-1);}
          舉個(gè)例子
          1.計(jì)算"book"的hashcode
          十進(jìn)制 : 3029737
          二進(jìn)制 : 101110001110101110 1001

          2.HashMap長度是默認(rèn)的16,length - 1的結(jié)果
          十進(jìn)制 : 15
          二進(jìn)制 : 1111

          3.把以上兩個(gè)結(jié)果做與運(yùn)算
          101110001110101110 1001 & 1111 = 1001
          1001的十進(jìn)制 : 9,所以 index=9

          結(jié)論:hash算法最終得到的index結(jié)果,取決于hashcode值的最后幾位
          現(xiàn)在,我們假設(shè)HashMap的長度是10,重復(fù)剛才的運(yùn)算步驟:
          hashcode : 101110001110101110 1001
          length - 1 :                                      1001
          index :                                             1001

          再換一個(gè)hashcode 101110001110101110 1111 試試:
          hashcode : 101110001110101110 1111
          length - 1 :                                      1001
          index :                                             1001

          從結(jié)果可以看出,雖然hashcode變化了,但是運(yùn)算的結(jié)果都是1001,也就是說,當(dāng)HashMap長度為10的時(shí)候,有些index結(jié)果的出現(xiàn)幾率會(huì)更大而有些index結(jié)果永遠(yuǎn)不會(huì)出現(xiàn)(比如0111),這樣就不符合hash均勻分布的原則

          反觀長度16或者其他2的冪,length - 1的值是所有二進(jìn)制位全為1,這種情況下,index的結(jié)果等同于hashcode后幾位的值,只要輸入的hashcode本身分布均勻,hash算法的結(jié)果就是均勻的。

          結(jié)論:HashMap的默認(rèn)長度為16和規(guī)定數(shù)組長度為2的冪,是為了降低hash碰撞的幾率。HashMap 容量為什么總是為 2 的次冪?推薦看下。
          2.HashMap擴(kuò)容限制的負(fù)載因子為什么是0.75呢?為什么不能是0.1或者1呢?
          由HashMap的put方法中實(shí)現(xiàn)中的addEntry的實(shí)現(xiàn)代碼可知當(dāng)數(shù)組長度達(dá)到限制條件的閾值就要進(jìn)行數(shù)組的擴(kuò)容。
          擴(kuò)容的方式是:

          新建一個(gè)長度為之前數(shù)組2倍的新的數(shù)組,然后將當(dāng)前的Entry數(shù)組中的元素全部傳輸過去,擴(kuò)容后的新數(shù)組長度為之前的2倍,所以擴(kuò)容相對(duì)來說是個(gè)耗資源的操作。

          擴(kuò)容的觸發(fā)條件:

          閾值 = 數(shù)組默認(rèn)的長度 x 負(fù)載因子(閾值=16x0.75=12)
          threshold = (int)(capacity * loadFactor);void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//當(dāng)size超過臨界閾值threshold,并且即將發(fā)生哈希沖突時(shí)進(jìn)行擴(kuò)容 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); }
          createEntry(hash, key, value, bucketIndex);}

          由上面的內(nèi)容可知,

          • 如果負(fù)載因子為0.5甚至更低的可能的話,最后得到的臨時(shí)閾值明顯會(huì)很小,這樣的情況就會(huì)造成分配的內(nèi)存的浪費(fèi),存在多余的沒用的內(nèi)存空間,也不滿足了哈希表均勻分布的情況。

          • 如果負(fù)載因子達(dá)到了1的情況,也就是Entry數(shù)組存滿了才發(fā)生擴(kuò)容,這樣會(huì)出現(xiàn)大量的哈希沖突的情況,出現(xiàn)鏈表過長,因此造成get查詢數(shù)據(jù)的效率。

          • 因此選擇了0.5~1的折中數(shù)也就是0.75,均衡解決了上面出現(xiàn)的情況。



          五、JDK8下的HashMap的實(shí)現(xiàn)

          區(qū)別


          • 使用一個(gè)Node數(shù)組取代了JDK7的Entry數(shù)組來存儲(chǔ)數(shù)據(jù),這個(gè)Node可能是鏈表結(jié)構(gòu),也可能是紅黑樹結(jié)構(gòu);

          • 如果插入的元素key的hashcode值相同,那么這些key也會(huì)被定位到Node數(shù)組的同一個(gè)格子里,如果不超過8個(gè)使用鏈表存儲(chǔ);

          • 超過8個(gè),會(huì)調(diào)用treeifyBin函數(shù),將鏈表轉(zhuǎn)換為紅黑樹。那么即使所有key的hashcode完全相同,由于紅黑樹的特點(diǎn),查找某個(gè)特定元素,也只需要O(logn)的開銷。



          上圖是示意圖,主要是描述結(jié)構(gòu),不會(huì)達(dá)到這個(gè)狀態(tài)的,因?yàn)檫@么多數(shù)據(jù)的時(shí)候早就擴(kuò)容了。


          put

          • 和 Java7 稍微有點(diǎn)不一樣的地方就是,Java7 是先擴(kuò)容后插入新值的,Java8 先插值再擴(kuò)容,不過這個(gè)不重要。


          get

          • 計(jì)算 key 的 hash 值,根據(jù) hash 值找到對(duì)應(yīng)數(shù)組下標(biāo): hash & (length-1)

          • 判斷數(shù)組該位置處的元素是否剛好就是我們要找的,如果不是,走第三步

          • 判斷該元素類型是否是 TreeNode,如果是,用紅黑樹的方法取數(shù)據(jù),如果不是,走第四步 遍歷鏈表,直到找到相等(==或equals)的 key


          六、CurrentHashMap的原理

          由于HashMap是線程不同步的,雖然處理數(shù)據(jù)的效率高,但是在多線程的情況下存在著安全問題,因此設(shè)計(jì)了CurrentHashMap來解決多線程安全問題。

          HashMap在put的時(shí)候,插入的元素超過了容量(由負(fù)載因子決定)的范圍就會(huì)觸發(fā)擴(kuò)容操作,就是rehash,這個(gè)會(huì)重新將原數(shù)組的內(nèi)容重新hash到新的擴(kuò)容數(shù)組中,在多線程的環(huán)境下,存在同時(shí)其他的元素也在進(jìn)行put操作,如果hash值相同,可能出現(xiàn)同時(shí)在同一數(shù)組下用鏈表表示,造成閉環(huán),導(dǎo)致在get時(shí)會(huì)出現(xiàn)死循環(huán),所以HashMap是線程不安全的。

          JDK7下的CurrentHashMap


          在JDK1.7版本中,ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu)是由一個(gè)Segment數(shù)組和多個(gè)HashEntry組成,主要實(shí)現(xiàn)原理是實(shí)現(xiàn)了鎖分離的思路解決了多線程的安全問題,如下圖所示:


          Segment數(shù)組的意義就是將一個(gè)大的table分割成多個(gè)小的table來進(jìn)行加鎖,也就是上面的提到的鎖分離技術(shù),而每一個(gè)Segment元素存儲(chǔ)的是HashEntry數(shù)組+鏈表,這個(gè)和HashMap的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)一樣。
          ConcurrentHashMap 與HashMap和Hashtable 最大的不同在于:put和 get 兩次Hash到達(dá)指定的HashEntry,第一次hash到達(dá)Segment,第二次到達(dá)Segment里面的Entry,然后在遍歷entry鏈表.

          初始化


          ConcurrentHashMap的初始化是會(huì)通過位與運(yùn)算來初始化Segment的大小,用ssize來表示,源碼如下所示
          private static final int DEFAULT_CONCURRENCY_LEVEL = 16;private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // For serialization compatibility // Emulate segment calculation from previous version of this class int sshift = 0; int ssize = 1; while (ssize < DEFAULT_CONCURRENCY_LEVEL) { ++sshift; ssize <<= 1; } int segmentShift = 32 - sshift; int segmentMask = ssize - 1;

          因?yàn)閟ize用位于運(yùn)算來計(jì)算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關(guān)concurrencyLevel的取值,當(dāng)然concurrencyLevel最大只能用16位的二進(jìn)制來表示,即65536,換句話說,Segment的大小最多65536個(gè),沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認(rèn)為 DEFAULT_CONCURRENCY_LEVEL =16

          每一個(gè)Segment元素下的HashEntry的初始化也是按照位與運(yùn)算來計(jì)算,用cap來表示,如下:
          int cap = 1;while (cap < c) cap <<= 1
          上所示,HashEntry大小的計(jì)算也是2的N次方(cap <<=1), cap的初始值為1,所以HashEntry最小的容量為2


          put操作

          staticclass Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; final float loadFactor; Segment(float lf) { this.loadFactor = lf; }}

          從上Segment的繼承體系可以看出,Segment實(shí)現(xiàn)了ReentrantLock,也就帶有鎖的功能,當(dāng)執(zhí)行put操作時(shí),會(huì)進(jìn)行第一次key的hash來定位Segment的位置,如果該Segment還沒有初始化,即通過CAS操作進(jìn)行賦值,然后進(jìn)行第二次hash操作,找到相應(yīng)的HashEntry的位置,這里會(huì)利用繼承過來的鎖的特性,在將數(shù)據(jù)插入指定的HashEntry位置時(shí)(鏈表的尾端),會(huì)通過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,如果獲取成功就直接插入相應(yīng)的位置,如果已經(jīng)有線程獲取該Segment的鎖,那當(dāng)前線程會(huì)以自旋的方式(如果不了解自旋鎖,請(qǐng)參考:自旋鎖原理及java自旋鎖)去繼續(xù)的調(diào)用tryLock()方法去獲取鎖,超過指定次數(shù)就掛起,等待喚醒。

          關(guān)注微信公眾號(hào):Java技術(shù)棧,在后臺(tái)回復(fù):java,可以獲取我整理的 N 篇最新Java 教程,都是干貨。


          get操作


          ConcurrentHashMap的get操作跟HashMap類似,只是ConcurrentHashMap第一次需要經(jīng)過一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進(jìn)行對(duì)比,成功就返回,不成功就返回null

          size 返回ConcurrentHashMap元素大小


          計(jì)算ConcurrentHashMap的元素大小是一個(gè)有趣的問題,因?yàn)樗遣l(fā)操作的,就是在你計(jì)算size的時(shí)候,他還在并發(fā)的插入數(shù)據(jù),可能會(huì)導(dǎo)致你計(jì)算出來的size和你實(shí)際的size有相差(在你return size的時(shí)候,插入了多個(gè)數(shù)據(jù)),要解決這個(gè)問題,JDK1.7版本用兩種方案
          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(); }}
          1、第一種方案他會(huì)使用不加鎖的模式去嘗試多次計(jì)算ConcurrentHashMap的size,最多三次,比較前后兩次計(jì)算的結(jié)果,結(jié)果一致就認(rèn)為當(dāng)前沒有元素加入,計(jì)算的結(jié)果是準(zhǔn)確的.
          2、第二種方案是如果第一種方案不符合,他就會(huì)給每個(gè)Segment加上鎖,然后計(jì)算ConcurrentHashMap的size返回.


          JDK8的ConcurrentHashMap


          JDK1.8的實(shí)現(xiàn)已經(jīng)摒棄了Segment的概念,而是直接用Node數(shù)組+鏈表+紅黑樹的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn),并發(fā)控制使用Synchronized和CAS來操作,整個(gè)看起來就像是優(yōu)化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數(shù)據(jù)結(jié)構(gòu),但是已經(jīng)簡化了屬性,只是為了兼容舊版本.


          在深入JDK1.8的put和get實(shí)現(xiàn)之前要知道一些常量設(shè)計(jì)和數(shù)據(jù)結(jié)構(gòu),這些是構(gòu)成ConcurrentHashMap實(shí)現(xiàn)結(jié)構(gòu)的基礎(chǔ),下面看一下基本屬性:

          // node數(shù)組最大容量:2^30=1073741824private static final int MAXIMUM_CAPACITY = 1 << 30;// 默認(rèn)初始值,必須是2的幕數(shù)private static final int DEFAULT_CAPACITY = 16//數(shù)組可能最大值,需要與toArray()相關(guān)方法關(guān)聯(lián)static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//并發(fā)級(jí)別,遺留下來的,為兼容以前的版本private static final int DEFAULT_CONCURRENCY_LEVEL = 16;// 負(fù)載因子private static final float LOAD_FACTOR = 0.75f;// 鏈表轉(zhuǎn)紅黑樹閥值,> 8 鏈表轉(zhuǎn)換為紅黑樹static final int TREEIFY_THRESHOLD = 8;//樹轉(zhuǎn)鏈表閥值,小于等于6(tranfer時(shí),lc、hc=0兩個(gè)計(jì)數(shù)器分別++記錄原bin、新binTreeNode數(shù)量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))static final int UNTREEIFY_THRESHOLD = 6;static final int MIN_TREEIFY_CAPACITY = 64;private static final int MIN_TRANSFER_STRIDE = 16;private static int RESIZE_STAMP_BITS = 16;// 2^15-1,help resize的最大線程數(shù)private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 32-16=16,sizeCtl中記錄size大小的偏移量private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;// forwarding nodes的hash值static final int MOVED = -1;// 樹根節(jié)點(diǎn)的hash值static final int TREEBIN = -2;// ReservationNode的hash值static final int RESERVED = -3;// 可用處理器數(shù)量static final int NCPU = Runtime.getRuntime().availableProcessors();//存放node的數(shù)組transient volatile Node<K,V>[] table;/*控制標(biāo)識(shí)符,用來控制table的初始化和擴(kuò)容的操作,不同的值有不同的含義 *當(dāng)為負(fù)數(shù)時(shí):-1代表正在初始化,-N代表有N-1個(gè)線程正在 進(jìn)行擴(kuò)容 *當(dāng)為0時(shí):代表當(dāng)時(shí)的table還沒有被初始化 *當(dāng)為正數(shù)時(shí):表示初始化或者下一次進(jìn)行擴(kuò)容的大小private transient volatile int sizeCtl;

          JDK8的Node

          Node是ConcurrentHashMap存儲(chǔ)結(jié)構(gòu)的基本單元,繼承于HashMap中的Entry,用于存儲(chǔ)數(shù)據(jù),Node數(shù)據(jù)結(jié)構(gòu)很簡單,就是一個(gè)鏈表,但是只允許對(duì)數(shù)據(jù)進(jìn)行查找,不允許進(jìn)行修改
          源代碼如下:
          static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next;
          Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; }
          public final K getKey() { return key; } public final V getValue() { return val; } public final int hashCode() { return key.hashCode() ^ val.hashCode(); } public final String toString(){ return key + "=" + val; } public final V setValue(V value) { throw new UnsupportedOperationException(); }
          public final boolean equals(Object o) { Object k, v, u; Map.Entry<?,?> e; return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?,?>)o).getKey()) != null && (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u))); }
          /** * Virtualized support for map.get(); overridden in subclasses. */ Node<K,V> find(int h, Object k) { Node<K,V> e = this; if (k != null) { do { K ek; if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; } while ((e = e.next) != null); } return null; } }

          TreeNode

          TreeNode繼承于Node,但是數(shù)據(jù)結(jié)構(gòu)換成了二叉樹結(jié)構(gòu),它是紅黑樹的數(shù)據(jù)的存儲(chǔ)結(jié)構(gòu),用于紅黑樹中存儲(chǔ)數(shù)據(jù),當(dāng)鏈表的節(jié)點(diǎn)數(shù)大于8時(shí)會(huì)轉(zhuǎn)換成紅黑樹的結(jié)構(gòu),他就是通過TreeNode作為存儲(chǔ)結(jié)構(gòu)代替Node來轉(zhuǎn)換成黑紅樹源代碼如下
          static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red;
          TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent) { super(hash, key, val, next); this.parent = parent; }
          Node<K,V> find(int h, Object k) { return findTreeNode(h, k, null); }
          /** * Returns the TreeNode (or null if not found) for the given key * starting at given root. */ final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) { if (k != null) { TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> q; TreeNode<K,V> pl = p.left, pr = p.right; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (pk != null && k.equals(pk))) return p; else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) p = (dir < 0) ? pl : pr; else if ((q = pr.findTreeNode(h, k, kc)) != null) return q; else p = pl; } while (p != null); } return null; } }

          TreeBin

          TreeBin從字面含義中可以理解為存儲(chǔ)樹形結(jié)構(gòu)的容器,而樹形結(jié)構(gòu)就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉(zhuǎn)換黑紅樹的一些條件和鎖的控制.


          總結(jié)和思考

          其實(shí)可以看出JDK1.8版本的ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu)已經(jīng)接近HashMap,相對(duì)而言,ConcurrentHashMap只是增加了同步的操作來控制并發(fā),從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹,相對(duì)而言,總結(jié)如下思考

          1、JDK1.8的實(shí)現(xiàn)降低鎖的粒度,JDK1.7版本鎖的粒度是基于Segment的,包含多個(gè)HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節(jié)點(diǎn))

          2、JDK1.8版本的數(shù)據(jù)結(jié)構(gòu)變得更加簡單,使得操作也更加清晰流暢,因?yàn)橐呀?jīng)使用synchronized來進(jìn)行同步,所以不需要分段鎖的概念,也就不需要Segment這種數(shù)據(jù)結(jié)構(gòu)了,由于粒度的降低,實(shí)現(xiàn)的復(fù)雜度也增加了

          3、JDK1.8使用紅黑樹來優(yōu)化鏈表,基于長度很長的鏈表的遍歷是一個(gè)很漫長的過程,而紅黑樹的遍歷效率是很快的,代替一定閾值的鏈表,這樣形成一個(gè)最佳拍檔

          4、JDK1.8為什么使用內(nèi)置鎖synchronized來代替重入鎖ReentrantLock,我覺得有以下幾點(diǎn)

          5、因?yàn)榱6冉档土耍谙鄬?duì)而言的低粒度加鎖方式,synchronized并不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能通過Condition來控制各個(gè)低粒度的邊界,更加的靈活,而在低粒度中,Condition的優(yōu)勢(shì)就沒有了

          6、JVM的開發(fā)團(tuán)隊(duì)從來都沒有放棄synchronized,而且基于JVM的synchronized優(yōu)化空間更大,使用內(nèi)嵌的關(guān)鍵字比使用API更加自然

          7、在大量的數(shù)據(jù)操作下,對(duì)于JVM的內(nèi)存壓力,基于API的ReentrantLock會(huì)開銷更多的內(nèi)存,雖然不是瓶頸,但是也是一個(gè)選擇依據(jù)


           最后給讀者整理了一份大廠面試真題,需要的可掃碼微信加備注:“大廠面試”獲取。





          END


          后臺(tái)回復(fù)“面試” “資料” 領(lǐng)取一份干貨,數(shù)百技術(shù)面試手冊(cè)等你
          開發(fā)者技術(shù)前線 ,匯集技術(shù)前線快訊和關(guān)注行業(yè)趨勢(shì),大廠干貨,是開發(fā)者經(jīng)歷和成長的優(yōu)秀指南
          好文點(diǎn)個(gè)在看吧!


          瀏覽 38
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  A片视频免费看 | 大香蕉国产在线观看 | 国产无码豆花 | 天天综合人人 | 国产精品第一操逼视频 |