ConcurrentHashMap詳解
目錄
原始碼為jdk1.7
ConcurrentHashMap介紹
ConcurrentHashMap 是concurrent包下的一個集合類。它是執行緒安全的雜湊表。它是通過“分段鎖”來實現多執行緒下的安全問題。它將雜湊表分成了不同的段內使用了可重入鎖(ReentrantLock ),不同執行緒只在一個段記憶體線上程的競爭。它不會對整個雜湊表加鎖。
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {
它繼承了AbstractMap類,實現了ConcurrentMap,Serializable介面
public abstract class AbstractMap<K,V> implements Map<K,V> {
可以看出AbstractMap類繼承了Map介面,也就是說存放著鍵值對,還有實現了Map介面那些方法。
ConcurrentHashMap底層資料結構
ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap的結構。它內部擁有一個HashEntry陣列,陣列中的每個元素又是一個連結串列;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
Segment繼承了可重入鎖。
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; HashEntry(K key, int hash, HashEntry<K,V> next, V value) { this.key = key; this.hash = hash; this.next = next; this.value = value; } @SuppressWarnings("unchecked") static final <K,V> HashEntry<K,V>[] newArray(int i) { return new HashEntry[i]; } }
HashEntry這個類有key、value、next、hash四個屬性。HashEntry中的value被volatile修飾,這樣在多執行緒讀寫過程中能夠保持它們的可見性。
ConcurrentHashMap部分分析
ConcurrentHashMap具有的屬性
//預設容量大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//預設載入因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//預設併發數量(segments陣列的大小)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大併發數量
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
static final int RETRIES_BEFORE_LOCK = 2;
final int segmentMask;
final int segmentShift;
//segments陣列
final Segment<K,V>[] segments;
//迭代器相關
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;
建構函式
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//引數的有效性判斷
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//如果concurrencyLevel大於最大併發量,將其值設為最大併發量。(segments陣列的大小)
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
//sshift記錄ssize左移的次數
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//c為每個為table陣列分配的容量
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);
}
concurrencyLevel的作用就是用來計算segments陣列的容量大小。先計算出“大於或等於concurrencyLevel的最小的2的N次方值”,然後將其儲存為“segments的容量大小(ssize)”。
put()方法
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}
如果value為null,丟擲異常。不允許值為空。通過key得到hash值。將key-value鍵值對新增到Segment片段中。
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;
// 根據”hash值“獲取”HashEntry陣列中對應的HashEntry連結串列“
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重複,則覆蓋舊值
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 {
//否則建立新的HashEntry節點
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//如果容量大於閾值,需要重hash
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//解鎖
unlock();
}
return oldValue;
}
put()的作用是將key-value鍵值對插入到“當前Segment對應的HashEntry中”,在插入前它會獲取Segment對應的互斥鎖,插入後會釋放鎖。
rehash()的作用是將”Segment的容量“變為”原始的Segment容量的2倍“。 在將原始的資料拷貝到“新的Segment”中後,會將新增加的key-value鍵值對新增到“新的Segment”中。
get方法
獲取key值對應的hash值,在對應的segment裡面獲取 。
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;
}
可以看到get方法並沒有加鎖
get(Object key)的作用是返回key在ConcurrentHashMap裡的值。首先根據key計算出hash值,獲取key對應的Segment片段。如果Segment不為null,則在Segment片段的HashEntry連結串列中遍歷這個連結串列找到對應的HashEntry節點。
由於遍歷過程中其他執行緒可能對連結串列結構做了調整,因此get和containsKey返回的可能是過時的資料,這一點是ConcurrentHashMap在弱一致性上的體現。
ConcurrentHashMap與HashMap、HashTable的區別
- ConcurrentHashMap 、HashTable不允許值為null,值為null時丟擲異常,HashMap允許值為null
- HashTable所有方法都加鎖synchronized,為執行緒安全的。HashMap不保證執行緒安全。ConcurrentHashMap採用了分段鎖,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。
- ConcurrentHashMap為弱一致性,在get方法時有可能獲得過時的資料。