1.爲什麼使用ConcurrentHashMap
(1)HashMap是線程不安全的:我們知道HashMap實際上封裝了一個Entry單鏈表來維護衝突值,但是如果單線程訪問,那麼通過鍵找到索引,再通過索引計算hash值找到這個單向節點鏈,然後遍歷到節點的後繼爲null,就可以結束循環遍歷了。但是在多線程訪問的情況下,可能一個線程已經遍歷到節點的後繼爲null了,其他線程繼續往節點鏈裏面插入數據,導致一個線程遍歷節點的後繼永遠不爲null,造成死循環。導致最後的CPU利用率接近100%。
(2)HashTable效率太低:HashTable採用隱式鎖Synchronized來保證線程安全,但是如果一個線程往Hash表裏放入元素或者讀取元素,那麼其他線程的讀寫將會阻塞。
(3)ConcurrentHashMap的特點:採用分段鎖的方式,讓一個hash表裏面分爲多把鎖,將一個hash表裏面的數據分爲多塊,雖然一個線程佔用其中的一個鎖的一塊區域,但是其他鎖和其他區域,其他線程也可以訪問,提高了效率,這樣鎖的細粒度更高了。
2.ConcurrentHashMap所屬包
package java.util.concurrent;
3.ConcurrentHashMap繼承與實現關係
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable
4.ConcurrentHashMap的結構圖
5.HashEntry節點的數據結構
/**
* 節點鏈中節點的數據結構
*/
static final class HashEntry<K,V> {
//節點對應的hash值
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;
}
/**
* 設置節點的後繼結點
*/
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
}
6.Segment片段
(1)片段類的屬性和構造方法:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
/**
* 在準備鎖定段操作之前,可能阻塞之前嘗試使用預掃描的最大次數。
* 在多處理器上,使用有限數量的重試來維護在定位節點時獲取的高速緩存。
*/
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* 可以線程間訪問的table
*/
transient volatile HashEntry<K,V>[] table;
/**
* 元素的數量
*/
transient int count;
/**
* 修改片段的次數
*/
transient int modCount;
/**
* 臨界值,如果實際大小超過了臨界值,就使用容量*加載因子重新計算來擴容
*/
transient int threshold;
/**
* 加載因子
*/
final float loadFactor;
//通過加載因子,臨界值,HashEntry節點來初始化片段
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
(2)片段類的方法:
rehash() 方法:
/**
*將表的大小擴增,重新計算hash值,然後將指定的節點添加到新表中
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
//獲取舊錶
HashEntry<K,V>[] oldTable = table;
//獲取舊錶的長度
int oldCapacity = oldTable.length;
//新表的容量爲舊錶的容量的2倍
int newCapacity = oldCapacity << 1;
//獲取臨界值
threshold = (int)(newCapacity * loadFactor);
//構造新的HashEntry數組
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//長度掩碼
int sizeMask = newCapacity - 1;
//遍歷舊錶
for (int i = 0; i < oldCapacity ; i++) {
//獲取舊錶的節點HashEntry
HashEntry<K,V> e = oldTable[i];
//如果節點不爲null
if (e != null) {
//獲取節點的後繼結點next
HashEntry<K,V> next = e.next;
//通過掩碼和hash值獲取下標索引
int idx = e.hash & sizeMask;
//如果後繼結點爲null
if (next == null)
//給新表賦值節點
newTable[idx] = e;
else { //如果後繼結點不爲null
//新表的下標索引對應的HashEntry值
HashEntry<K,V> lastRun = e;
//舊下標:記錄新表的下標索引
int lastIdx = idx;
//從後繼結點開始遍歷
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
//計算後繼結點的下標索引
int k = last.hash & sizeMask;
//如果後繼結點對應的下標和之前節點的下標不同
if (k != lastIdx) {
//將後繼結點的下標賦值給舊下標
lastIdx = k;
//將後繼結點的下標對應的HashEntry節點值賦值給舊值
lastRun = last;
}
}
//給新表賦值節點
newTable[lastIdx] = lastRun;
//遍歷新表,爲了獲取後繼結點,構造每個新節點
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
//獲取value值
V v = p.value;
//獲取hash值
int h = p.hash;
//獲取下標索引
int k = h & sizeMask;
//獲取下標索引對應的HashEntry節點值
HashEntry<K,V> n = newTable[k];
//構造新的HashEntry節點並賦值新表
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//獲取節點下標索引
int nodeIndex = node.hash & sizeMask; // add the new node
//設置節點的後繼結點
node.setNext(newTable[nodeIndex]);
//爲新表中的下標索引設置節點值
newTable[nodeIndex] = node;
table = newTable;
}
scanAndLockForPut() 方法:
/**
* 通過key查找節點鏈中對應的節點
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//通過指定的片段和hash值來獲取HashEntry節點
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) {
//如果鏈表中不存在對應的HashEntry節點
if (e == null) {
if (node == null)
//創建HashEntry節點
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
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.重新開始
retries = -1;
}
}
return node;
}
put() 方法:
//存入元素的操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
/* 如果當前片段的鎖已經被其他線程持有了,
* 那麼獲取的節點node爲null
* 如果沒有被其他線程持有,那麼就直接獲取節點鏈中對應的節點
*/
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//獲取舊錶
HashEntry<K,V>[] tab = table;
//通過與位運算計算出下標索引index
int index = (tab.length - 1) & hash;
//獲取給定表tab中指定下標index對應的頭節點first
HashEntry<K,V> first = entryAt(tab, index);
//遍歷節點鏈表
for (HashEntry<K,V> e = first;;) {
//如果節點不爲空
if (e != null) {
K k;
//如果存在鍵相同或者存在hash值相同並且鍵也相同
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
//獲取鍵對應的節點的值
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
//修改次數加1
++modCount;
}
break;
}
//找下一個節點
e = e.next;
}
else {
//如果node不爲空
if (node != null)
//設置node節點的後繼結點爲first
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
//節點鏈中的節點數量加1
int c = count + 1;
//如果片段的容量大於了臨界值並且表的長度小於最大容量
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//重新計算hash值
rehash(node);
else
//爲指定表的指定下標設置節點值node
setEntryAt(tab, index, node);
//修改的次數加1
++modCount;
//重新設置鏈表中的節點數量
count = c;
//釋放對象
oldValue = null;
break;
}
}
} finally {
//將當前線程獲取的鎖釋放
unlock();
}
return oldValue;
}
7.ConcurrentHashMap屬性字段
/**
* 默認初始化容量爲16
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 默認的加載因子爲0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 默認併發級別爲16(鎖的個數)
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 最大容量爲2^30次冪
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 最小的分段容量爲2(需要爲2的n次冪)
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/**
* 最大分成2^16段
*/
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
/**
* 同步重試的次數
*/
static final int RETRIES_BEFORE_LOCK = 2;
8.ConcurrentHashMap的構造方法
構造方法1:
/**
* 構造一個帶有默認初始化容量、默認加載因子、默認併發級別的空映射
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
構造方法2:
/**
* 構造一個帶有指定容量、默認加載因子、默認併發級別的空映射
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
構造方法3:
/**
* 構造一個帶有指定容量、指定加載因子、默認併發級別的空映射
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
構造方法4:
/**
* 通過一個指定的映射關係Map來構造一個映射
*/
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);
}
構造方法5:
/**
* 構造一個指定容量、指定加載因子、指定併發級別的新映射
*/
@SuppressWarnings("unchecked")
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
/* 如果加載因子小於0或者初始化容量小於0或者併發級別小於0
* 就拋出非法參數異常
*/
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
/* 如果鎖的數量大於最大分段數
* 就將鎖的數量設置爲最大分段數
*/
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// 變量sshift用於記錄ssize增加的次數
int sshift = 0;
//變量ssize用於記錄小於鎖的數量的最大偶數
int ssize = 1;
while (ssize < concurrencyLevel) {
//sshift增加1,
++sshift;
//ssize擴大爲原來的2倍
ssize <<= 1;
}
//散列運算的位數
this.segmentShift = 32 - sshift;
//散列運算的掩碼
this.segmentMask = ssize - 1;
/* 如果初始化容量大於最大容量
* 那麼就設置初始化容量爲最大容量
*/
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//初始化容量除以鎖的數量得到分段數量
int c = initialCapacity / ssize;
/* 如果initialCapacity/ssize能整除
* 那麼c*ssize =initialCapacity
* 如果initialCapacity/ssize不能整除有餘數
* 那麼就將分段數+1
*/
if (c * ssize < initialCapacity)
++c;
//獲取最小分段數量
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
//最小分段數量擴大爲原來的2倍且小於c
cap <<= 1;
//初始化數組中每個元素對應的片段Segment對象
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
//初始化Segment數組
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
9.ConcurrentHashMap的方法
put() 方法:
/**
* 將指定的鍵映射到該表中指定的值
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment<K,V> s;
//如果值爲null,就拋出空指針異常
if (value == null)
throw new NullPointerException();
//通過鍵生成hash值
int hash = hash(key);
//hash值右移片段偏移量個位,然後按位與片段掩碼
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//通過索引獲取對應的片段
s = ensureSegment(j);
//將元素存入到片段
return s.put(key, hash, value, false);
}
ensureSegment()
方法:
/**
* 通過給定的索引獲取片段
*/
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//通過u獲取對應的片段,如果片段不爲null
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//獲取第一個片段
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
//獲取第一個片段中表的長度
int cap = proto.table.length;
//獲取第一個片段中的加載因子
float lf = proto.loadFactor;
//獲取臨界值
int threshold = (int)(cap * lf);
//初始化HashEntry數組
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
//通過u獲取對應的片段,如果獲取的片段爲null(第二次檢查)
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//構造表中的片段
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
get() 方法:
/**
* 如果映射不包含鍵的映射,則返回指定鍵被映射到的值
*/
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
//通過鍵獲取hash值
int h = hash(key);
/**
* 將hash值右移segmentShift個位
* 然後進行和segmentMask的按位與運算
*/
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//通過上面計算的值u獲取片段值,如果不爲null,並且片段值對應的表也不爲null
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//遍歷片段中的節點鏈HashEntry
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;
}
10.閱讀總結
(1)ConcurrentHashMap是一種採用分段鎖的方式來實現的。
(2)ConcurrentHashMap默認初始化容量爲16,默認加載因子爲0.75。
(3)ConcurrentHashMap如果初始化空間不夠,將進行擴容,擴容爲原來的2倍,重新計算hash值,並將舊元素移動到新空間。
(4)ConcurrentHashMap中的節點HashEntry本身採用單鏈表的數據結構進行實現的。
-----------------------------該源碼爲jdk1.7版本的