多線程知識梳理(7) ConcurrentHashMap 實現原理

1、前言

ConcurrentHashMap是線程安全而且高效的HashMap,其它的相似容器有如下缺點:java

  • HashMap在併發執行put操做時,會致使Entry鏈表造成環形數據結構,就會產生死循環獲取Entry
  • HashTable使用synchronized來保證線程安全,但在線程競爭激烈的狀況下HashTable的效率很是低下。

ConcurrentHashMap高效的緣由在於它採用 鎖分段技術,首先將數據分紅一段一段地存儲,而後給每段數據配一把鎖,當一個線程佔用鎖而且訪問一段數據的時候,其餘段的數據也能被其餘線程訪問。算法

2、 ConcurrentHashMap 的結構

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成:編程

  • Segment是一種可重入鎖,在ConcurrentHashMap裏面扮演鎖的角色。
  • HashEntry則用於存儲鍵值對數據。

一個ConcurrentHashMap裏包含一個Segment數組,它的結構和HashMap相似,是一種數組和鏈表結構。 數組

segment
一個 Segment裏包含一個 HashEntry數組,每一個 HashEntry是一個鏈表結構的元素,每一個 Segment守護着一個 HashEntry裏的元素,當對 HashEntry數組的數據進行修改時,必須首先得到與它對應的 Segment鎖。

Segment 結構

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile int count;
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry<K,V>[] table;
    final float loadFactor;
}
複製代碼
  • countSegment中元素的數量
  • modCount:對table的大小形成影響的操做的數量
  • threshold:閾值,Segment裏面元素的數量超過這個值依舊就會對Segment進行擴容
  • table:鏈表數組,數組中的每個元素表明了一個鏈表的頭部
  • loadFactor:負載因子,用於肯定threshold

HashEntry 結構

static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
}
複製代碼

2.1 初始化

ConcurrentHashMap的初始化方法是經過initialCapacityloadFactorconcurrencyLevel等幾個參數來初始化segment數組、段偏移量segmentShift、段掩碼segmentMask和每一個segment裏的HashEntry來實現的。安全

2.1.1 初始化 segment 數組

初始化segment的源代碼以下,它會計算出:數據結構

  • ssizesegment數組的長度
  • segmentShiftsshift等於ssize1向左移位的次數,segmentShift等於32-sshiftsegmentShift用於 定位參與散列運算的位數
  • segmentMask散列運算的掩碼,等於ssize-1
if (concurrencyLevel > MAX_SEGMENTS)
    concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
//計算 segments 數組的長度,它是大於等於 concurrencyLevel 的最小的 2 的 N 次方。
while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
複製代碼

2.1.2 初始化每一個 segment

輸入參數initialCapacityConcurrentHashMap的初始化容量,loadFactor是每一個segment的負載因子,在構造方法裏經過這兩個參數來初始化數組中的每一個segment多線程

if (initialCapacity < MAXIMUM_CAPACITY) {
    initialCapacity = MAXIMUM_CAPACITY;
}
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity) {
    ++c;
}
int cap = 1;
while (cap < c) {
    cap <<= 1;
}
for (int i = 0; i < this.segments.length; i++) {
    this.segments[i] = new Segment<K, V>(cap, loadFactor);
}
複製代碼

cap 是 segment 裏 HashEntry 數組的長度,它等於initialCapacity / ssize,若是c大於1,就會取大於等於c2N次方。segment的容量threshold等於(int) cap * loadFactor,默認狀況下initialCapacity等於16ssize等於16loadFactor等於0.75,所以cap等於1threshold等於0併發

2.2 定位 segment

在插入和獲取元素的時候,必須先經過散列算法定位到SegmentConcurrentHashMap會首先對元素的hashCode()進行一次再散列。框架

private static int hash(int h) {
    h += (h << 15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h << 3);
    h ^= (h >>> 6);
    h += (h << 2) + (h << 14);
    return h ^ (h >>> 16);
}
複製代碼

再散列的目的是減小散列衝突,使元素可以均勻地分佈在不一樣的Segment上,從而提升容器的存取效率。ssh

2.3 操做

2.3.1 get 操做

segmentget操做過程爲:先進行一次再散列,而後使用這個散列值經過散列運算定位到Segment,再經過散列算法定位到元素。

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}
複製代碼

get操做的高效之處在於整個get過程不須要加鎖,除非讀到的值爲空才加鎖重讀。在它的get方法裏,將要使用的共享變量都定義成volatile類型,如用於統計當前segment大小的count字段和用於存儲值的HashEntryvalue,定義成volatile的變量,可以在線程之間保持可見性,可以被多線程同時讀,而且保證不會讀到過時的值,在get操做裏,只須要讀而不須要寫共享變量countvalue,因此能夠不用加鎖。

transient volatile int count;
volatile V value;
複製代碼

2.3.2 put 操做

因爲put方法須要對共享變量進行寫入,因此爲了線程安全,在操做共享變量時必須加鎖。put方法首先定位到Segment,而後在Segment裏進行插入操做。插入操做須要經歷兩個步驟:

  • 判斷是否須要對Segment裏的HashEntry數組進行擴容
  • 定位添加元素的位置,而後將其放在HashEntry數組裏

2.3.3 size 操做

若是要統計整個ConcurrentHashMap裏元素的大小,就必須統計全部Segment元素的大小後求和,雖然每一個Segment的全局變量count是一個volatile變量,在相加時能夠獲取最新值,可是不能保證以前累加過的Segment大小不發生變化。

所以,ConcurrentHashMap會先嚐試2次經過不鎖住Segment的方式來統計各個Segment大小,若是統計的過程當中,容器的count發生了變化,則再採用加鎖的方式來統計全部Segment的大小。

檢測容器大小是否發生變化的原理爲:在putremoveclean方法裏操做元素前會將變量modCount進行加1,那麼在統計size先後比較modCount是否發生變化,從而得知容器的大小是否發生變化。

3、參考文獻

<<Java併發編程的藝術>> - Java併發容器和框架

相關文章
相關標籤/搜索