HashMap和ConcurrentHashMap的知識總結(jié)

Github issues:https://github.com/littlejoyo/Blog/issues/

個人博客:https://littlejoyo.github.io/

微信公眾號:Joyo說

哈希表(hash table)也叫散列表,是一種非常重要的數(shù)據(jù)結(jié)構(gòu),應用場景及其豐富,許多緩存技術(shù)(比如memcached)的核心其實就是在內(nèi)存中維護一張大的哈希表。

一、什么是哈希表

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

  • 數(shù)組
    采用一段連續(xù)的存儲單元來存儲數(shù)據(jù)。對于指定下標的查找,時間復雜度為O(1);
    通過給定值進行查找,需要遍歷數(shù)組,逐一比對給定關(guān)鍵字和數(shù)組元素,時間復雜度為O(n),當然,對于有序數(shù)組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復雜度提高為O(logn);
    對于一般的插入刪除操作,涉及到數(shù)組元素的移動,其平均復雜度也為O(n)

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

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

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

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


哈希表

比如我們要新增或查找某個元素,我們通過把當前元素的關(guān)鍵字 通過某個函數(shù)映射到數(shù)組中的某個位置,通過數(shù)組下標一次定位就可完成操作。
存儲位置 = f(關(guān)鍵字)
其中,這個函數(shù)f一般稱為哈希函數(shù),這個函數(shù)的設計好壞會直接影響到哈希表的優(yōu)劣。
查找操作同理,先通過哈希函數(shù)計算出實際存儲地址,然后從數(shù)組中對應地址取出即可。

二、哈希沖突

通過哈希函數(shù)得出的實際存儲地址相同怎么辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發(fā)現(xiàn)已經(jīng)被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞。

哈希函數(shù)的設計至關(guān)重要,好的哈希函數(shù)會盡可能地保證 計算簡單和散列地址分布均勻,但是不可能設計出一個絕對完美的哈希函數(shù),我們需要清楚的是,數(shù)組是一塊連續(xù)的固定長度的內(nèi)存空間,再好的哈希函數(shù)也不能保證得到的存儲地址絕對不發(fā)生沖突。

哈希沖突的解決方案有多種:開放定址法(發(fā)生沖突,繼續(xù)尋找下一塊未被占用的存儲地址),再散列函數(shù)法,鏈地址法.
HashMap即是采用了鏈地址法.

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

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

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

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry是HashMap中的一個靜態(tài)內(nèi)部類。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結(jié)構(gòu)
        int hash;//對key的hashcode值進行hash運算后得到的值,存儲在Entry,避免重復計算

        /**
         * 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的結(jié)構(gòu)
  • 解決沖突的鏈表的長度影響到HashMap查詢的效率
    簡單來說,HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數(shù)組位置不含鏈表(當前entry的next指向null),那么對于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表,對于添加操作,其時間復雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對于查找操作來講,仍需遍歷鏈表,然后通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現(xiàn)越少,性能才會越好。

  • 發(fā)生沖突關(guān)于entry節(jié)點插入鏈表還是鏈頭呢?
    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)相應的key值的話,則會調(diào)用addEntry方法在鏈表添加一個Entry,重點就在與addEntry方法是如何插入鏈表的,addEntry方法源碼如下:

 void addEntry(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)造了一個新的Entry對象(構(gòu)造方法的最后一個參數(shù)傳入了當前的Entry鏈表),然后直接用這個新的Entry對象取代了舊的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可以看出確實是把原本的鏈表直接鏈在了新建的Entry對象的后邊,可以斷定是插入頭部。

JDK8
還是繼續(xù)查看put方法的源碼查看插入節(jié)點的代碼:

//e是p的下一個節(jié)點
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;
}

從這段代碼中可以很顯然地看出當?shù)竭_鏈表尾部(即p是鏈表的最后一個節(jié)點)時,e被賦為null,會進入這個分支代碼,然后會用newNode方法建立一個新的節(jié)點插入尾部。

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

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

查看HashMap計算hashcode的方法獲取存儲的位置:
為了減少hash值的碰撞,需要實現(xiàn)一個盡量均勻分布的hash函數(shù),在HashMap中通過利用key的hashcode值,來進行位運算


存儲的流程
 /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

舉個例子
1.計算"book"的hashcode
十進制 : 3029737
二進制 : 101110001110101110 1001

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

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

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


再換一個hashcode 101110001110101110 1111 試試:
hashcode : 101110001110101110 1111
length - 1 :????????????????????????????????????1001
index : ????????????????????????????????????????? 1001
從結(jié)果可以看出,雖然hashcode變化了,但是運算的結(jié)果都是1001,也就是說,當HashMap長度為10的時候,有些index結(jié)果的出現(xiàn)幾率會更大而有些index結(jié)果永遠不會出現(xiàn)(比如0111),這樣就不符合hash均勻分布的原則
反觀長度16或者其他2的冪,length - 1的值是所有二進制位全為1,這種情況下,index的結(jié)果等同于hashcode后幾位的值,只要輸入的hashcode本身分布均勻,hash算法的結(jié)果就是均勻的。


結(jié)論

  • HashMap的默認長度為16和規(guī)定數(shù)組長度為2的冪,是為了降低hash碰撞的幾率。

2.HashMap擴容限制的負載因子為什么是0.75呢?為什么不能是0.1或者1呢?

由HashMap的put方法中實現(xiàn)中的addEntry的實現(xiàn)代碼可知當數(shù)組長度達到限制條件的閾值就要進行數(shù)組的擴容。
擴容的方式是:
新建一個長度為之前數(shù)組2倍的新的數(shù)組,然后將當前的Entry數(shù)組中的元素全部傳輸過去,擴容后的新數(shù)組長度為之前的2倍,所以擴容相對來說是個耗資源的操作。
擴容的觸發(fā)條件:
閾值 = 數(shù)組默認的長度 x 負載因子(閾值=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);//當size超過臨界閾值threshold,并且即將發(fā)生哈希沖突時進行擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

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

  • 如果負載因子為0.5甚至更低的可能的話,最后得到的臨時閾值明顯會很小,這樣的情況就會造成分配的內(nèi)存的浪費,存在多余的沒用的內(nèi)存空間,也不滿足了哈希表均勻分布的情況。
  • 如果負載因子達到了1的情況,也就是Entry數(shù)組存滿了才發(fā)生擴容,這樣會出現(xiàn)大量的哈希沖突的情況,出現(xiàn)鏈表過長,因此造成get查詢數(shù)據(jù)的效率。
  • 因此選擇了0.5~1的折中數(shù)也就是0.75,均衡解決了上面出現(xiàn)的情況。

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

區(qū)別:

  • 使用一個Node數(shù)組取代了JDK7的Entry數(shù)組來存儲數(shù)據(jù),這個Node可能是鏈表結(jié)構(gòu),也可能是紅黑樹結(jié)構(gòu);
  • 如果插入的元素key的hashcode值相同,那么這些key也會被定位到Node數(shù)組的同一個格子里,如果不超過8個使用鏈表存儲;
  • 超過8個,會調(diào)用treeifyBin函數(shù),將鏈表轉(zhuǎn)換為紅黑樹。那么即使所有key的hashcode完全相同,由于紅黑樹的特點,查找某個特定元素,也只需要O(logn)的開銷。


    JDK8的HashMap

上圖是示意圖,主要是描述結(jié)構(gòu),不會達到這個狀態(tài)的,因為這么多數(shù)據(jù)的時候早就擴容了。

put

  • 和 Java7 稍微有點不一樣的地方就是,Java7 是先擴容后插入新值的,Java8 先插值再擴容,不過這個不重要。

get

  • 計算 key 的 hash 值,根據(jù) hash 值找到對應數(shù)組下標: hash & (length-1)
  • 判斷數(shù)組該位置處的元素是否剛好就是我們要找的,如果不是,走第三步
  • 判斷該元素類型是否是 TreeNode,如果是,用紅黑樹的方法取數(shù)據(jù),如果不是,走第四步 遍歷鏈表,直到找到相等(==或equals)的 key

六、CurrentHashMap的原理

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

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

JDK7下的CurrentHashMap

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

CurrentHashMap的結(jié)構(gòu)

Segment數(shù)組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術(shù),而每一個Segment元素存儲的是HashEntry數(shù)組+鏈表,這個和HashMap的數(shù)據(jù)存儲結(jié)構(gòu)一樣。

ConcurrentHashMap 與HashMap和Hashtable 最大的不同在于:put和 get 兩次Hash到達指定的HashEntry,第一次hash到達Segment,第二次到達Segment里面的Entry,然后在遍歷entry鏈表.

初始化

ConcurrentHashMap的初始化是會通過位與運算來初始化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;

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

每一個Segment元素下的HashEntry的初始化也是按照位與運算來計算,用cap來表示,如下:

int cap = 1;
while (cap < c)
    cap <<= 1

上所示,HashEntry大小的計算也是2的N次方(cap <<=1), cap的初始值為1,所以HashEntry最小的容量為2

put操作

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

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

get

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

size 返回ConcurrentHashMap元素大小

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

JDK8的ConcurrentHashMap

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


JDK8的ConcurrentHashMap

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

// node數(shù)組最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認初始值,必須是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ā)級別,遺留下來的,為兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 負載因子
private static final float LOAD_FACTOR = 0.75f;
// 鏈表轉(zhuǎn)紅黑樹閥值,> 8 鏈表轉(zhuǎn)換為紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//樹轉(zhuǎn)鏈表閥值,小于等于6(tranfer時,lc、hc=0兩個計數(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é)點的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;
/*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
 *當為負數(shù)時:-1代表正在初始化,-N代表有N-1個線程正在 進行擴容
 *當為0時:代表當時的table還沒有被初始化
 *當為正數(shù)時:表示初始化或者下一次進行擴容的大小
private transient volatile int sizeCtl;

JDK8的Node

Node是ConcurrentHashMap存儲結(jié)構(gòu)的基本單元,繼承于HashMap中的Entry,用于存儲數(shù)據(jù),Node數(shù)據(jù)結(jié)構(gòu)很簡單,就是一個鏈表,但是只允許對數(shù)據(jù)進行查找,不允許進行修改
源代碼如下:

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ù)的存儲結(jié)構(gòu),用于紅黑樹中存儲數(shù)據(jù),當鏈表的節(jié)點數(shù)大于8時會轉(zhuǎn)換成紅黑樹的結(jié)構(gòu),他就是通過TreeNode作為存儲結(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從字面含義中可以理解為存儲樹形結(jié)構(gòu)的容器,而樹形結(jié)構(gòu)就是指TreeNode,所以TreeBin就是封裝TreeNode的容器,它提供轉(zhuǎn)換黑紅樹的一些條件和鎖的控制.

總結(jié)和思考

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

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

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

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

  • JDK1.8為什么使用內(nèi)置鎖synchronized來代替重入鎖ReentrantLock,我覺得有以下幾點
    1.因為粒度降低了,在相對而言的低粒度加鎖方式,synchronized并不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能通過Condition來控制各個低粒度的邊界,更加的靈活,而在低粒度中,Condition的優(yōu)勢就沒有了
    2.JVM的開發(fā)團隊從來都沒有放棄synchronized,而且基于JVM的synchronized優(yōu)化空間更大,使用內(nèi)嵌的關(guān)鍵字比使用API更加自然
    3.在大量的數(shù)據(jù)操作下,對于JVM的內(nèi)存壓力,基于API的ReentrantLock會開銷更多的內(nèi)存,雖然不是瓶頸,但是也是一個選擇依據(jù)
    參考資料:

  • HashMap實現(xiàn)原理及源碼分析

  • Hashmap死循環(huán)原因總結(jié)

  • ConcurrentHashMap原理分析(1.7與1.8)

  • CurrentHashMap原理與應用(JDK7)

  • java-并發(fā)-ConcurrentHashMap高并發(fā)機制-jdk1.8

微信公眾號

掃一掃關(guān)注Joyo說公眾號,共同學習和研究開發(fā)技術(shù)。

關(guān)注我
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。