摘要
我們都知道HashMap是線程不安全的,擴容時有可能還會產生死循環!那麼有沒有一種比較安全的HashMap給我們使用呢?JDK其實已經爲我們提供了一種實現,它就是ConcurrentHashMap;
介紹
一個支持檢索的完全併發性和更新的可調預期併發性的哈希表。 這個類遵守與Hashtable相同的功能規範,幷包含與Hashtable的每個方法對應的方法版本。 然而,即使所有操作都是線程安全的,檢索操作也不需要鎖定,並且不支持以阻止所有訪問的方式鎖定整個表。 在依賴Hashtable的線程安全性但不依賴其同步細節的程序中,這個類與Hashtable完全可互操作。
檢索操作(包括get)通常不會阻塞,因此可能與更新操作(包括put和remove)重疊。 檢索反映了最近完成的更新操作在開始時保持的結果。 對於聚合操作(如putAll和clear),併發檢索可能只反映某些條目的插入或刪除。 類似地,Iterators和Enumerations返回的元素反映了哈希表在創建迭代器/枚舉時的某個時點或自創建以來的狀態。 它們不會拋出ConcurrentModificationException。 但是,迭代器被設計爲一次只能被一個線程使用。
更新操作之間允許的併發性由可選的concurrencyLevel構造函數參數(默認16)指導,該參數被用作內部大小調整的提示。 對錶進行內部分區,以嘗試在沒有爭用的情況下允許指定數量的併發更新。 因爲哈希表中的位置基本上是隨機的,所以實際的併發性會有所不同。 理想情況下,您應該選擇一個值來容納儘可能多的併發修改表的線程。 使用比您需要的高得多的值可能會浪費空間和時間,而使用較低的值可能會導致線程爭用。 但是在一個數量級內的高估和低估通常不會產生太明顯的影響。 當知道只有一個線程會修改,而其他所有線程只會讀取時,值1是合適的。 另外,調整這個或任何其他類型的散列表的大小是一個相對較慢的操作,因此,如果可能的話,最好在構造函數中提供預期表大小的估計。
這個類及其視圖和迭代器實現了Map和Iterator接口的所有可選方法。 與Hashtable類似但又不同於HashMap,該類不允許將null用作鍵或值。
基本策略是將表細分爲Segments,每個Segments本身是一個併發可讀的哈希表。 爲了減少內存佔用,除了一個段之外的所有段只在第一次需要時才構造(參見ensureSegment)。 爲了在惰性構造的情況下保持可見性,訪問段以及段表的元素必須使用volatile訪問,這是通過不安全的方法segmentAt等完成的。 它們提供了AtomicReferenceArrays的功能,但減少了間接級別。 另外,鎖操作中對錶元素和條目“next”字段的volatile寫操作使用更便宜的“lazySet”形式(通過putOrderedObject),因爲這些寫操作之後總是會釋放鎖,以保持表更新的順序一致性。
歷史提示:該類的上一個版本嚴重依賴於“final”字段,這避免了一些volatile讀取,但代價是大量的初始佔用空間。 該設計的一些殘餘(包括強制構造段0)存在以確保串行兼容性。
源碼解析
(1)、類定義
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {
...
}
類定義沒有什麼特別之處,就是實現ConcurrentMap接口,在ConcurrentMap定義了幾個原子方法
(2)、常量定義
/**
* 該表的默認初始容量,當沒有在構造中指定時使用
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 該表的默認加載因子,當沒有在構造函數中指定時使用。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 該表的默認併發級別,當沒有在構造函數中指定時使用。
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 最大容量,如果一個較大的值由帶參數的構造函數中的任何一個隱式指定,則使用該值。 必須是2的冪<= 1<<30,以確保條目可以使用整數進行索引。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 每個段表的最小容量。 必須是2的冪,至少爲2,以避免在惰性構造後的下一次使用中立即調整大小。
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/**
* 允許的最大段數; 用於綁定構造函數參數。 必須是2的冪,小於1 << 24。
*/
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative 略顯保守
/**
* 在訴諸鎖定之前,在大小和containsValue方法上的非同步重試次數。 這用於避免在表經歷連續修改時進行無界重試,這將導致無法獲得準確的結果。
*/
static final int RETRIES_BEFORE_LOCK = 2;
(3)、字段定義
//與此實例關聯的隨機值,應用於鍵的散列代碼,使散列衝突更難找到。
private transient final int hashSeed = randomHashSeed(this);
/**
* Mask value for indexing into segments. The upper bits of a
* key's hash code are used to choose the segment.
* 索引段的掩碼值。 鍵的哈希碼的上位用於選擇段。
*/
final int segmentMask;
/**
* 在段內移位索引值。
*/
final int segmentShift;
/**
* 每個段都是一個專門的哈希表。用於存放真實數據
*/
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
(4)、內部類HashEntry
/**
* ConcurrentHashMap列表條目。 請注意,它從未作爲用戶可見的Map.Entry導出。
*/
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* 用volatile寫語義設置下一個字段。 (請參閱上面關於putOrderedObject的使用。)
*/
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
// Unsafe mechanics
static final sun.misc.Unsafe UNSAFE;
static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = HashEntry.class;
nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
(6)、構造方法
/**
* 創建一個新的空映射,帶有指定的初始容量,以及默認的負載因子(0.75)和concurrencyLevel(16)。
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* 創建一個新的空映射,具有默認的初始容量(16)、負載因子(0.75)和併發級別(16)。
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* 創建具有與給定映射相同映射的新映射。 創建的映射的容量是給定映射的1.5倍或16(哪個更大),以及默認的負載因子(0.75)和concurrencyLevel (16)
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
putAll(m);
}
/**
* 使用指定的初始容量、負載因子和併發級別創建一個新的空映射。
*
* @param initialCapacity 最初的能力。 該實現執行內部大小調整以適應這麼多元素。
* @param loadFactor 負載因子閾值,用於控制調整容量大小。當元素的平均數量超過這個閾值時,可以執行調整大小。
* @param concurrencyLevel 併發更新線程的估計數目。 該實現執行內部大小調整以嘗試容納這麼多線程。
*/
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//參數合法性檢查
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//併發級別如果操作最大允許的段,則強制設置爲最大允許的段大小
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments 找到兩倍大小的最佳匹配參數
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
(7)、內部類Segment
段是哈希表的特殊版本。 這個子類取巧地繼承了ReentrantLock,只是爲了簡化一些鎖並避免單獨構造。
段維護一個始終保持一致狀態的條目列表表,所以可以在不鎖定的情況下讀取(通過對段和表的volatile讀取)。 這需要在調整表大小時複製節點,因此仍然使用舊版本表的讀者可以遍歷舊列表。
該類只定義需要鎖定的可變方法。 除了前面提到的,這個類的方法執行ConcurrentHashMap方法的每段版本。 (其他方法直接集成到ConcurrentHashMap方法中。) 這些可變的方法通過scanAndLock和scanAndLockForPut方法在爭用時使用一種控制旋轉的形式。 這些函數將trylock與遍歷穿插在一起以定位節點。 它的主要好處是在獲取鎖時吸收緩存丟失(這在散列表中很常見),這樣一旦獲得鎖,遍歷就會更快。 我們實際上並不使用找到的節點,因爲它們必須在鎖定狀態下重新獲取,以確保更新的順序一致性(在任何情況下都可能無法檢測到過時),但它們通常會更快地重新定位。 此外,如果沒有找到節點,scanAndLockForPut投機性地創建一個新的節點用於put。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
/**
* 預掃描中在可能阻塞之前嘗試鎖定的最大次數,爲鎖定段操作做準備。 在多處理器上,使用有限的重試次數來維護定位節點時獲得的緩存。
*/
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* 每段的hash表。 通過提供volatile語義的entryAt/setEntryAt訪問元素。
*/
transient volatile HashEntry<K,V>[] table;
/**
* 元素的數量。 只能在鎖中或在其他保持可見性的volatile讀取中訪問。
*/
transient int count;
/**
* 在這個段中發生變化的操作的總數。 儘管這可能會溢出32位,但它爲CHM isEmpty()和size()方法中的穩定性檢查提供了足夠的準確性。
* 只能在鎖中或在其他保持可見性的volatile讀取中訪問。
*/
transient int modCount;
/**
* 當表的大小超過此閾值時,表將重新散列。 (該字段的值總是(int)(capacity * loadFactor)。)
*/
transient int threshold;
/**
* 哈希表的負載因子。 即使這個值對於所有的段都是相同的,它也會被複制以避免需要連接到外部對象。
*/
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
....
}
從上面可以看出分段,這裏面的段 其實是一個個鎖。
(8)、get方法
/**
* 返回指定鍵映射到的值,如果這個映射不包含該鍵的映射,則返回{@code null}。
*/
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
//定位段地址
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//通過地址獲取內存可見的分段
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
(9)、put方法
/**
* 將指定的鍵映射到該表中的指定值。 鍵和值都不能爲空。
*
* 可以通過調用get方法來檢索該值,該方法使用的鍵等於原始鍵。
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return 前一個值與鍵關聯,或null(如果鍵沒有映射的話)
* @throws NullPointerException 如果指定的鍵或值爲空
*/
public V put(K key, V value) {
Segment<K,V> s;
//如果值爲空則拋出異常
if (value == null)
throw new NullPointerException();
//如果值爲空,該方法也會拋出異常
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
// nonvolatile; recheck
if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null)
// in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
調用分段的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//對當前段加鎖;如果成功,則進行進行後續操作;否則,調用scanAndLockForPut方法進行掃描
HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
//判斷節點是否key和hash相等,如果是,則進行value替換
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//如果不存在該Key的映射,則在鏈表頭部插入一個新節點
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//如果達到閾值,則擴容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
//將新節點放入tab對於的槽位
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
調用scanAndLockForPut方法,進行自旋
/**
* 當試圖獲取鎖時,掃描包含給定鍵的節點,如果返回,創建並返回一個鍵,確保鎖被持有。
* 與大多數方法不同,對equals方法的調用不會被篩選:由於遍歷速度無關緊要,我們還可以幫助預熱相關的代碼和訪問。
*
* @return a new node if key not found, else null
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//獲取hash對應槽位的第一個節點
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // 定位節點時爲負
//嘗試獲取鎖
while (!tryLock()) {
//獲取失敗
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
//如果該槽位上的第一個節點爲空,則新建一個節點
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
//如果該槽位上的第一個節點非空,且key和當前put的相同
else if (key.equals(e.key))
retries = 0;
//否則,繼續遍歷
else
e = e.next;
}
//如果達到嘗試獲取鎖的次數達到最大允許嘗試次數,則直接加鎖
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
(10)、remove方法
/**
* 從映射中移除該鍵(及其對應的值)。 如果鍵不在映射中,此方法將不執行任何操作。
*
* @param key the key that needs to be removed
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>
* @throws NullPointerException if the specified key is null
*/
public V remove(Object key) {
int hash = hash(key);
//通過hash定位到segment
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
調用segment.remove方法
/**
* 刪除; 僅當value爲null時匹配key,否則兩者都匹配。
*/
final V remove(Object key, int hash, Object value) {
//嘗試加鎖
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
/**
* 當試圖獲取刪除或替換操作的鎖時,掃描包含給定鍵的節點。 返回時,保證鎖被持有。
* 注意,即使沒有找到鍵,我們也必須鎖定,以確保更新的順序一致性。
*/
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
int retries = -1;
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
}
數據結構
總結:
(1)、ConcurrentHashMap1.7底層數據結構主要是分段鎖+數組+鏈表; 分段鎖Segment通過繼承ReentrantLock來實現;
(2)、進行put、remove等操作時,它會將hash值對於的段進行加鎖,然後進行相應的操作。加鎖的時候,剛開始會實行嘗試獲取自旋,然後超過最大嘗試次數後,直接加鎖。
(3)、另外,底層實現上使用volatile,sun.misc.Unsafe的getObjectVolatile()、putOrderedObject()、getObject()等等一些底層方法來實現。關於Unsafe這一塊的知識,後期再進行補充!