Java併發容器—ConcurrentHashMap
1.JDK1.7版本
jdk1.7的實現結構圖如下所示
[外鏈圖片轉存失敗(img-Fy93gKVd-1566009849146)(/home/shidongxuan/.config/Typora/typora-user-images/1566002052129.png)]
CurrentHashMap是由Segment數組和HashEntry數組結構組成. Segment是一種可重入鎖(ReentrantLock), 在ConcurrentHashMap裏扮演鎖的角色, HashEntry用於存儲鍵值對數據. 綜上, ConcurrentHashMap採用了分段鎖技術, 其中Segment繼承與ReentrantLock. 不像HashTable那樣不管是put操作還是get操作都需要做同步處理, 理論上ConcurrentHashMap支持段級別的線程併發, 當一個線程佔用鎖訪問一個Segment時, 不會影響其他Segment
1>put方法
//先定位到Segment, 之後在對應的Segment上進行具體的put
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;
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);
}
//具體的put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//先嚐試對segment加鎖, 如果加鎖成功, 那麼node=null,如果加鎖失敗則調用
//scanAndLockForPut方法去獲取鎖, 這個方法中, 獲取鎖後會返回對應的HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//優化點, 這時已經加了鎖,只會有一個線程訪問table, 但是table是volatile修飾的
//所以將其賦值給一個局部變量, 減少volatile帶來的開銷
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
//獲取到對應位置上的HashEntry鏈表
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
//e不爲空, 說明出現了hash衝突, 遍歷當前鏈表
//如果找到一個key相同的, 則根據onlyIfAbsent判斷是否覆蓋value值
K k;
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 {
//說明e爲空, 一種可能是在上面遍歷到最後沒有找到key相同的, 也可能是一開始就是null
//第一次沒有獲取到鎖, 通過scanAndLockForPut方法獲取到了鎖,放到敵營位置上
if (node != null)
node.setNext(first);
else
//否則新new出一個節點對象
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;//節點數+1
//判斷是否需要擴容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
//數組無須擴容, 就直接插入node到指定index位置
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
下面是獲取鎖的scanAndLockForPut方法
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
//如果嘗試加鎖失敗,那麼就對segment[hash]對應的鏈表進行遍歷找到需要put的這個entry所在的鏈表中的位置,
//這裏之所以進行一次遍歷找到坑位,主要是爲了通過遍歷過程將遍歷過的entry全部放到CPU高速緩存中,
//這樣在獲取到鎖了之後,再次進行定位的時候速度會十分快,這是在線程無法獲取到鎖前並等待的過程中的一種預熱方式。
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//獲取鎖失敗,初始時retries=-1必然開始先進入第一個if
if (retries < 0) {//<1>
if (e == null) { //<1.1>
//e=null代表兩種意思,第一種就是遍歷鏈表到了最後,仍然沒有發現指定key的entry;
//第二種情況是剛開始時確實太過entryForHash找到的HashEntry就是空的,即通過hash找到的table中對應位置鏈表爲空
//當然這裏之所以還需要對node==null進行判斷,是因爲有可能在第一次給node賦值完畢後,然後預熱準備工作已經搞定,
//然後進行循環嘗試獲取鎖,在循環次數還未達到<2>以前,某一次在條件<3>判斷時發現有其它線程對這個segment進行了修改,
//那麼retries被重置爲-1,從而再一次進入到<1>條件內,此時如果再次遍歷到鏈表最後時,因爲上一次遍歷時已經給node賦值過了,
//所以這裏判斷node是否爲空,從而避免第二次創建對象給node重複賦值。
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))//<1.2> 遍歷過程發現鏈表中找到了我們需要的key的坑位
retries = 0;
else//<1.3> 當前位置對應的key不是我們需要的,遍歷下一個
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {//<2>
// 嘗試獲取鎖次數超過設置的最大值,直接進入阻塞等待,這就是所謂的有限制的自旋獲取鎖,
//之所以這樣是因爲如果持有鎖的線程要過很久才釋放鎖,這期間如果一直無限制的自旋其實是對系統性能有消耗的,
//這樣無限制的自旋是不利的,所以加入最大自旋次數,超過這個次數則進入阻塞狀態等待對方釋放鎖並獲取鎖。
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {//<3>
// 遍歷過程中,有可能其它線程改變了遍歷的鏈表,這時就需要重新進行遍歷了。
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
其核心思想就是通過MAX_SCAN_RETRIES控制自旋次數, 防止無限制的自旋和浪費資源. 這個方法的作用就是遍歷獲取所然後進行數據插入
2>rehash方法
/**
* Doubles size of table and repacks entries, also adding the
* given node to new table
* 對數組進行擴容,由於擴容過程需要將老的鏈表中的節點適用到新數組中,所以爲了優化效率,可以對已有鏈表進行遍歷,
* 對於老的oldTable中的每個HashEntry,從頭結點開始遍歷,找到第一個後續所有節點在新table中index保持不變的節點fv,
* 假設這個節點新的index爲newIndex,那麼直接newTable[newIndex]=fv,即可以直接將這個節點以及它後續的鏈表中內容全部直接複用copy到newTable中
* 這樣最好的情況是所有oldTable中對應頭結點後跟隨的節點在newTable中的新的index均和頭結點一致,那麼就不需要創建新節點,直接複用即可。
* 最壞情況當然就是所有節點的新的index全部發生了變化,那麼就全部需要重新依據k,v創建新對象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list 只有單個節點
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
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;
lastRun = last;
}
}//這個for循環就是找到第一個後續節點新的index不變的節點。
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//第一個後續節點新index不變節點前所有節點均需要重新創建分配。
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
3>ensureSegment方法
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;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // 以初始化時創建的第一個坑位的ss[0]作爲模版進行創建
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // 二次檢查是否有其它線程創建了這個Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
//這裏通過自旋的CAS方式對segments數組中偏移量爲u位置設置值爲s,這是一種不加鎖的方式,
//萬一有多個線程同時執行這一步,那麼只會有一個成功,而其它線程在看到第一個執行成功的線程結果後
//會獲取到最新的數據從而發現需要更新的坑位已經不爲空了,那麼就跳出while循環並返回最新的seg
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
這個方法核心是利用自旋CAS來創建對應的Segment, 這種思想是不加鎖保證線程安全.
4>get方法
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);//獲取key對應hash值
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//獲取對應h值存儲所在segments數組中內存偏移量
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//通過Unsafe中的getObjectVolatile方法進行volatile語義的讀,獲取到segments在偏移量爲u位置的分段Segment,
//並且分段Segment中對應table數組不爲空
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {//獲取h對應這個分段中偏移量爲xxx下的HashEntry的鏈表頭結點,然後對鏈表進行 遍歷
//###這裏第一次初始化通過getObjectVolatile獲取HashEntry時,獲取到的是主存中最新的數據,但是在後續遍歷過程中,有可能數據被其它線程修改
//從而導致其實這裏最終返回的可能是過時的數據,所以這裏就是ConcurrentHashMap所謂的弱一致性的體現,containsKey方法也一樣
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
這裏這個方法是弱一致性的, 所以可能會返回過時的數據
5>size方法
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // 是否溢出
long sum; // 存儲本次循環過程中計算得到的modCount的值
long last = 0L; // 存儲上一次遍歷過程中計算得到的modCount的和
int retries = -1; // first iteration isn't retry
try {
for (;;) {//無限for循環,結束條件就是任意前後兩次遍歷過程中modcount值的和是一樣的,說明第二次遍歷沒有做任何變化
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 {
//由於只有在retries等於RETRIES_BEFORE_LOCK時纔會執行強制加鎖,並且由於是用的retries++,
//所以強制加鎖完畢後,retries的值是一定會大於RETRIES_BEFORE_LOCK的,
//這樣就防止正常遍歷而沒進行加鎖時進行鎖釋放的情況
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
如果要統計整個ConcurrentHashMap中的元素個數, 將每個Segment中的元素數量加起來就好了, 正好Segment中的count是一個volatile變量. 但如果在累加的時候, 其他線程進行了插入或刪除操作, 結果就不準了. 所以最安全的方法是對所有Segment的put, remove, clean方法鎖住, 但是這種方法很低效.
因爲在累加count的過程中, 之前累加過的count發生變化的機率很小, 所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment的大小, 如果統計過程中count發生了變化, 則再採用加鎖的方式統計所有Segment的大小
2.JDK1.8版本
在jdk1.8中, ConcurrentHashMap通過一個Node<K,V>[]數組來保存添加到map中的鍵值對, 在同一個數組位置是通過鏈表或紅黑樹的形式來表示, 其實原理類似於jdk1.8中的HashMap
1>put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/*
* 當添加一對鍵值對的時候,首先會去判斷保存這些鍵值對的數組是不是初始化了,如果沒有的話就初始化數組
* 然後通過計算hash值來確定放在數組的哪個位置
* 如果這個位置爲空則直接添加,如果不爲空的話,則取出這個節點來
* 如果取出來的節點的hash值是MOVED(-1)的話,則表示當前正在對這個數組進行擴容,複製到新的數組,則當前線程也去幫助複製
* 最後一種情況就是,如果這個節點,不爲空,也不在擴容,則通過synchronized來加鎖,進行添加操作
* 然後判斷當前取出的節點位置存放的是鏈表還是樹
* 如果是鏈表的話,則遍歷整個鏈表,直到取出來的節點的key來個要放的key進行比較,如果key相等,並且key的hash值也相等的話,
* 則說明是同一個key,則覆蓋掉value,否則的話則添加到鏈表的末尾
* 如果是樹的話,則調用putTreeVal方法把這個元素添加到樹中去
* 最後在添加完成之後,會判斷在該節點處共有多少個節點(注意是添加前的個數),如果達到8個以上了的話,
* 則調用treeifyBin方法來嘗試將處的鏈表轉爲樹,或者擴容數組
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//K,V都不能爲空,否則的話跑出異常
int hash = spread(key.hashCode()); //取得key的hash值
int binCount = 0; //用來計算在這個節點總共有多少個元素,用來控制擴容或者轉移爲樹
for (Node<K,V>[] tab = table;;) { //
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //第一次put的時候table沒有初始化,則初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { /
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) //創建一個Node添加到數組中區,null表示的是下一個節點爲空
break; // no lock when adding to empty bin
}
/*
* 如果檢測到某個節點的hash值是MOVED,則表示正在進行數組擴張的數據複製階段,
* 則當前線程也會參與去複製,通過允許多線程複製的功能,一次來減少數組的複製所帶來的性能損失
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
/*
* 如果在這個位置有元素的話,就採用synchronized的方式加鎖,
* 如果是鏈表的話(hash大於0),就對這個鏈表的所有元素進行遍歷,
* 如果找到了key和key的hash值都一樣的節點,則把它的值替換到
* 如果沒找到的話,則添加在鏈表的最後面
* 否則,是樹的話,則調用putTreeVal方法添加到樹中去
*
* 在添加完之後,會對該節點上關聯的的數目進行判斷,
* 如果在8個以上的話,則會調用treeifyBin方法,來嘗試轉化爲樹,或者是擴容
*/
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) { //再次取出要存儲的位置的元素,跟前面取出來的比較
if (fh >= 0) { //取出來的元素的hash值大於0,當轉換爲樹之後,hash值爲-2
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { //遍歷這個鏈表
K ek;
if (e.hash == hash && //要存的元素的hash,key跟要存儲的位置的節點的相同的時候,替換掉該節點的value即可
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) //當使用putIfAbsent的時候,只有在這個key沒有設置值得時候才設置
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { //如果不是同樣的hash,同樣的key的時候,則判斷該節點的下一個節點是否爲空,
pred.next = new Node<K,V>(hash, key, //爲空的話把這個要加入的節點設置爲當前節點的下一個節點
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //表示已經轉化成紅黑樹類型了
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, //調用putTreeVal方法,將該元素添加到樹中去
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //當在同一個節點的數目達到8個的時候,則擴張數組或將給節點的數據轉爲tree
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //計數
return null;
}
2>擴容transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 構建一個nextTable,大小爲table兩倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//通過for自循環處理每個槽位中的鏈表元素,默認advace爲真,通過CAS設置transferIndex屬性值,並初始化i和bound值,i指當前處理的槽位序號,bound指需要處理的槽位邊界,先處理槽位15的節點;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) { // 遍歷table中的每一個節點
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // //如果所有的節點都已經完成複製工作 就把nextTable賦值給table 清空臨時對象nextTable
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //擴容閾值設置爲原來容量的1.5倍 依然相當於現在容量的0.75倍
return;
}
// 利用CAS方法更新這個擴容閾值,在這裏面sizectl值減一,說明新加入一個線程參與到擴容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//如果遍歷到的節點爲空 則放入ForwardingNode指針
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果遍歷到ForwardingNode節點 說明這個點已經被處理過了 直接跳過 這裏是控制併發擴容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { // 鏈表節點
int runBit = fh & n; // resize後的元素要麼在原地,要麼移動n位(n爲原capacity),詳解見:https://huanglei.rocks/coding/194.html#4%20resize()%E7%9A%84%E5%AE%9E%E7%8E%B0
Node<K,V> lastRun = f;
//以下的部分在完成的工作是構造兩個鏈表 一個是原鏈表 另一個是原鏈表的反序排列
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//在nextTable的i位置上插入一個鏈表
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一個鏈表
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
//設置advance爲true 返回到上面的while循環中 就可以執行i--操作
advance = true;
}
//對TreeBin對象進行處理 與上面的過程類似
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//構造正序和反序兩個鏈表
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// (1)如果lo鏈表的元素個數小於等於UNTREEIFY_THRESHOLD,默認爲6,則通過untreeify方法把樹節點鏈表轉化成普通節點鏈表;(2)否則判斷hi鏈表中的元素個數是否等於0:如果等於0,表示lo鏈表中包含了所有原始節點,則設置原始紅黑樹給ln,否則根據lo鏈表重新構造紅黑樹。
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); // tab[i]已經處理完了
advance = true;
}
}
}
}
}
}
如何在擴容時,併發地複製與插入?
- 遍歷整個table,當前節點爲空,則採用CAS的方式在當前位置放入fwd
- 當前節點已經爲fwd(with hash field “MOVED”),則已經有有線程處理完了了,直接跳過 ,這裏是控制併發擴容的核心
- 當前節點爲鏈表節點或紅黑樹,重新計算鏈表節點的hash值,移動到nextTable相應的位置(構建了一個反序鏈表和順序鏈表,分別放置在i和i+n的位置上)。移動完成後,用Unsafe.putObjectVolatile在tab的原位置賦爲爲fwd, 表示當前節點已經完成擴容。
3>get方法
讀取操作,不需要同步控制,比較簡單
- 空tab,直接返回null
- 計算hash值,找到相應的bucket位置,爲node節點直接返回,否則返回null
ublic V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
3.HashTable與ConcurrentHashMap對比
ConcurrentHashMap能完全替代HashTable嗎?
hash table雖然性能上不如ConcurrentHashMap,但並不能完全被取代,兩者的迭代器的一致性不同的, hash table的迭代器是強一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
- Hashtable的任何操作都會把整個表鎖住,是阻塞的。好處是總能獲取最實時的更新,比如說線程A調用putAll寫入大量數據,期間線程B調用get,線程B就會被阻塞,直到線程A完成putAll,因此線程B肯定能獲取到線程A寫入的完整數據。壞處是所有調用都要排隊,效率較低。
- ConcurrentHashMap 是設計爲非阻塞的。在更新時會局部鎖住某部分數據,但不會把整個表都鎖住。同步讀取操作則是完全非阻塞的。好處是在保證合理的同步前提下,效率很高。壞處 是嚴格來說讀取操作不能保證反映最近的更新。例如線程A調用putAll寫入大量數據,期間線程B調用get,則只能get到目前爲止已經順利插入的部分數據。
選擇哪一個,是在性能與數據一致性之間權衡。ConcurrentHashMap適用於追求性能的場景,大多數線程都只做insert/delete操作,對讀取數據的一致性要求較低。