在介紹ConcurrentHashMap原始碼之前,很有必要複習下java併發程式設計中的一些基礎知識,比如記憶體模型等。
儲存模型
併發程式設計中的三個概念
1、原子性
2、可見性
3、重排序
對HashMap在jdk8有所瞭解
對CAS有所瞭解
對內建鎖和顯示鎖等有所瞭解
jdk8對ConcurrentHashMap做了很大的調整,首先因為HashMap在jdk8已經做了資料結構上的優化,增加了紅黑樹,詳情可以參考我之前的部落格。所以,jdk7針對ConcurrentHashMap的改進,主要是增加了分段鎖Segment對HashEntity的控制,完美的解決了HashMap的安全問題,在JMM中有個名稱叫安全釋出,已經不適用了。那麼,在jdk8如果保持效能的情況下對其進行修改了?它到底做了那些事情呢?
因為ConcurrentHashMap涉及的內容太多,jdk8有六千多行程式碼,jdk7才一兩千行吧。所以我在想怎麼一步步的對其進行剖解,最後我還是覺得按照程式的思路來吧,首先我們跑個ConcurrentHashMap的程式,然後進行除錯,來一步步展開。
public static void main(String[] args) {
Map<String, String> cm = new ConcurrentHashMap<String, String>();
for (int i = 0; i < 14; i++) {
cm.put("key_" + i, "huaizuo_" + i);
}
}
首先初始化一個ConcurrentHashMap,因為我們是用預設建構函式,我們來看下初始化的一些重要的欄位,去掉英文註釋。
/**
* races. Updated via CAS.
* 記錄容器的容量大小,通過CAS更新
*/
private transient volatile long baseCount;
/**
* 這個sizeCtl是volatile的,那麼他是執行緒可見的,一個思考:它是所有修改都在CAS中進行,但是sizeCtl為什麼不設計成LongAdder(jdk8出現的)型別呢?
* 或者設計成AtomicLong(在高併發的情況下比LongAdder低效),這樣就能減少自己操作CAS了。
*
* 來看下注釋,當sizeCtl小於0說明有多個執行緒正則等待擴容結果,參考transfer函式
*
* sizeCtl等於0是預設值,大於0是擴容的閥值
*/
private transient volatile int sizeCtl;
/**
* 自旋鎖 (鎖定通過 CAS) 在調整大小和/或建立 CounterCells 時使用。 在CounterCell類更新value中會使用,功能類似顯示鎖和內建鎖,效能更好
* 在Striped64類也有應用
*/
private transient volatile int cellsBusy;
還有最重要的節點類Node,注意val和next是volatile型別
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
接下來我們要把元素put到ConcurrentHashMap中了,那麼我們來看下putVal的原始碼吧
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
//這邊加了一個迴圈,就是不斷的嘗試,因為在table的初始化和casTabAt用到了compareAndSwapInt、compareAndSwapObject
//因為如果其他執行緒正在修改tab,那麼嘗試就會失敗,所以這邊要加一個for迴圈,不斷的嘗試
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//1
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))//2
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)// a
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//這個地方設計非常的巧妙,內建鎖synchronized鎖住了f,因為f是指定特定的tab[i]的,
// 所以就鎖住了整行連結串列,這個設計跟分段鎖有異曲同工之妙,只是其他讀取操作需要用cas來保證
synchronized (f) {
if (tabAt(tab, i) == f) {//3
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
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,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);//轉化為紅黑樹
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
我們看到程式碼註釋中的1、2、3我特定標註的,因為這些操作都是按照CAS的,其中關鍵部分已經做了註釋,要正確取到真實資料需要知道變數所在的記憶體偏移量。
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
/*
*但是這邊為什麼i要等於((long)i << ASHIFT) + ABASE呢,計算偏移量
*ASHIFT是指tab[i]中第i個元素在相對於陣列第一個元素的偏移量,而ABASE就算第一陣列的記憶體素的偏移地址
*所以呢,((long)i << ASHIFT) + ABASE就算i最後的地址
* 那麼compareAndSwapObject的作用就算tab[i]和c比較,如果相等就tab[i]=v否則tab[i]=c;
*/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
關於sun.misc.Unsafe
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
//獲取ConcurrentHashMap這個物件欄位sizeCtl在記憶體中的偏移量
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(k.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(k.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(k.getDeclaredField("cellsBusy"));
Class<?> ck = CounterCell.class;
CELLVALUE = U.objectFieldOffset
(ck.getDeclaredField("value"));
Class<?> ak = Node[].class;
//可以獲取陣列第一個元素的偏移地址
ABASE = U.arrayBaseOffset(ak);
//arrayIndexScale可以獲取陣列的轉換因子,也就是陣列中元素的增量地址
//將arrayBaseOffset與arrayIndexScale配合使用,可以定位陣列中每個元素在記憶體中的位置。
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
還是繼續看put原始碼,看到//a註釋,當(fh = f.hash) == MOVED,說明f.hash值為-1(MOVED為-1的final),那麼如果hash什麼時候回等於-1呢?為什麼會有-1這種情況呢?這要涉及到ForwardingNode<K,V>類
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//MOVED 位-1,說明ForwardNode的節點的hash值為-1
super(MOVED, null, null, null);
this.nextTable = tab;
}
這個類是繼承Node類的,他在初始化的時候hash值傳了MOVED,我們知道ConcurrentHashMap在的資料結構是Table[]和連結串列組成,所以如果Table節點是ForwardNode節點的話那麼Hash的值就等於-1,那麼什麼時候Node會變成ForwardNode呢?就是在擴容的時候,舊的Table的節點會臨時用ForwardNode代替。待會會介紹。
我們還是繼續一步步看程式碼,看inputVal的註釋a,這個方法helpTransfer,如果執行緒進入到這邊說明已經有其他執行緒正在做擴容操作,這個是一個輔助方法
/**
* Helps transfer if a resize is in progress.
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//下面幾種情況和addCount的方法一樣,請參考addCount的備註
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
這邊如果table的某一個節點對應的連結串列超過一定的長度之後,就要把連結串列轉化為紅黑樹的操作我就不詳細的在這邊文章介紹了,對於轉化的操作其實和HashMap是一樣的,但是這裡涉及到併發,它其實也是通過synchronized和CAS來控制併發的。好了,當我們的putVal執行到addCount的時候
/**
* Adds to count, and if table is too small and not already
* resizing, initiates transfer. If already resizing, helps
* perform transfer if work is available. Rechecks occupancy
* after a transfer to see if another resize is already needed
* because resizings are lagging additions.
*
* @param x the count to add
* @param check if <0, don't check resize, if <= 1 only check if uncontended
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x) 每次竟來都baseCount都加1因為x=1
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//1
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//多執行緒CAS發生失敗的時候執行
fullAddCount(x, uncontended);//2
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//當條件滿足開始擴容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {//如果小於0說明已經有執行緒在進行擴容操作了
//一下的情況說明已經有在擴容或者多執行緒進行了擴容,其他執行緒直接break不要進入擴容操作
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))//如果相等說明擴容已經完成,可以繼續擴容
transfer(tab, nt);
}
//這個時候sizeCtl已經等於(rs << RESIZE_STAMP_SHIFT) + 2等於一個大的負數,這邊加上2很巧妙,因為transfer後面對sizeCtl--操作的時候,最多隻能減兩次就結束
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
看上面註釋1,每次都會對baseCount 加1,如果併發競爭太大,那麼可能導致U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x) 失敗,那麼為了提高高併發的時候baseCount可見性失敗的問題,又避免一直重試,這樣效能會有很大的影響,那麼在jdk8的時候是有引入一個類Striped64,其中LongAdder和DoubleAdder就是對這個類的實現。這兩個方法都是為解決高併發場景而生的,是AtomicLong的加強版,AtomicLong在高併發場景效能會比LongAdder差。但是LongAdder的空間複雜度會高點。
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//獲取當前執行緒的probe值作為hash值,如果0則強制初始化當前執行緒的Probe值,初始化的probe值不為0
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;//設定未競爭標記為true
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell如果當前沒有CounterCell就建立一個
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//這邊加上cellsBusy鎖
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;//釋放cellsBusy鎖,讓其他執行緒可以進來
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail wasUncontended為false說明已經發生了競爭,重置為true重新執行上面程式碼
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))//對cell的value值進行累計x(1)
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale 表明as已經過時,說明cells已經初始化完成,看下面,重置collide為false表明已經存在競爭
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale 下面的程式碼主要是給counterCells擴容,儘可能避免衝突
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {//表明counterCells還沒初始化,則初始化,這邊用cellsBusy加鎖
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))//最終如果上面的都失敗就把x累計到baseCount
break; // Fall back on using base
}
}
原始碼註釋寫著See LongAdder version for explanation。我上面已經做了註釋了,就不做更多解釋了。
回到addCount來,我們每次竟來都對baseCount進行加1當達到一定的容量時,就需要對table進行擴容。擴容方法就是transfer,這個方法稍微複雜一點,大部分的程式碼我都做了註釋
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*/
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];
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;
//迴圈的關鍵變數,判斷是否已經擴容完成,完成就return,退出迴圈
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//迴圈的關鍵i,i--操作保證了倒序遍歷陣列
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {//nextIndex=transferIndex=n=tab.length(預設16)
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//i<0說明已經遍歷完舊的陣列tab;i>=n什麼時候有可能呢?在下面看到i=n,所以目前i最大應該是n吧。
//i+n>=nextn,nextn=nextTab.length,所以如果滿足i+n>=nextn說明已經擴容完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {// a
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//利用CAS方法更新這個擴容閾值,在這裡面sizectl值減一,說明新加入一個執行緒參與到擴容操作,參考sizeCtl的註釋
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果有多個執行緒進行擴容,那麼這個值在第二個執行緒以後就不會相等,因為sizeCtl已經被減1了,所以後面的執行緒就只能直接返回,始終保證只有一個執行緒執行了 a(上面註釋a)
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;//finishing和advance保證執行緒已經擴容完成了可以退出迴圈
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)//如果tab[i]為null,那麼就把fwd插入到tab[i],表明這個節點已經處理過了
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)//那麼如果f.hash=-1的話說明該節點為ForwardingNode,說明該節點已經處理過了
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//這邊還對連結串列進行遍歷,這邊的的演算法和hashmap的演算法又不一樣了,這班是有點對半拆分的感覺
//把連結串列分表拆分為,hash&n等於0和不等於0的,然後分別放在新表的i和i+n位置
//次方法同hashmap的resize
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);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//把已經替換的節點的舊tab的i的位置用fwd替換,fwd包含nextTab
setTabAt(tab, i, fwd);
advance = true;
}//下面紅黑樹基本和連結串列差不多
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;
}
}
//判斷擴容後是否還需要紅黑樹結構
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);
advance = true;
}
}
}
}
}
}
值得細細品味的是,transfer的for迴圈是倒敘的,說明對table的遍歷是從table.length-1開始到0的。我覺得這段程式碼寫得太牛逼了,特別是
//利用CAS方法更新這個擴容閾值,在這裡面sizectl值減一,說明新加入一個執行緒參與到擴容操作,參考sizeCtl的註釋
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果有多個執行緒進行擴容,那麼這個值在第二個執行緒以後就不會相等,因為sizeCtl已經被減1了,所以後面的執行緒就只能直接返回,始終保證只有一個執行緒執行了 a(上面註釋a)
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;//finishing和advance保證執行緒已經擴容完成了可以退出迴圈
i = n; // recheck before commit
}
反正很多地方值得細細精讀。
那麼我想我已經把ConcurrentHashMap的一部分內容講完,包括新增元素putVal,擴容transfer等。那麼現在我們來看下get方法吧
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public 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)//如果eh=-1就說明e節點為ForWordingNode,這說明什麼,說明這個節點已經不存在了,被另一個執行緒正則擴容
//所以要查詢key對應的值的話,直接到新newtable找
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;
}
這個get請求,我們需要cas來保證變數的原子性。如果tab[i]正被鎖住,那麼CAS就會失敗,失敗之後就會不斷的重試。這也保證了get在高併發情況下不會出錯。
我們來分析下到底有多少種情況會導致get在併發的情況下可能取不到值。1、一個執行緒在get的時候,另一個執行緒在對同一個key的node進行remove操作;2、一個執行緒在get的時候,另一個執行緒正則重排table。可能導致舊table取不到值。
那麼本質是,我在get的時候,有其他執行緒在對同一桶的連結串列或樹進行修改。那麼get是怎麼保證同步性的呢?我們看到e = tabAt(tab, (n - 1) & h)) != null,在看下tablAt到底是幹嘛的:
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
它是對tab[i]進行原子性的讀取,因為我們知道putVal等對table的桶操作是有加鎖的,那麼一般情況下我們對桶的讀也是要加鎖的,但是我們這邊為什麼不需要加鎖呢?因為我們用了Unsafe的getObjectVolatile,因為table是volatile型別,所以對tab[i]的原子請求也是可見的。因為如果同步正確的情況下,根據happens-before原則,對volatile域的寫入操作happens-before於每一個後續對同一域的讀操作。所以不管其他執行緒對table連結串列或樹的修改,都對get讀取可見。用一張圖說明,協調讀-寫執行緒可見示意圖:
那麼好奇的我翻看了下jdk7的get方法是怎麼處理的,因為我們知道jdk7是沒有用到CAS操作和Unsafe類的,下面是jdk7的get方法
V get(Object key, int hash) {
if(count != 0) { // 首先讀 count 變數
HashEntry<K,V> e = getFirst(hash);
while(e != null) {
if(e.hash == hash && key.equals(e.key)) {
V v = e.value;
if(v != null)
return v;
// 如果讀到 value 域為 null,說明發生了重排序,加鎖後重新讀取
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}
為什麼我們在get的時候需要判斷count不等於0呢?如果是在HashMap的原始碼中是沒有這個判斷的,不用判斷不是也是可以的嗎?這個就是用到執行緒安全釋出情況下happens-before原則之volatile變數法則:對volatile域的寫入操作happens-before於每一個後續對同一域的讀操作,看下面的示意圖:
如果有讀java併發程式設計實戰這本書第16的話,我們知道要有時候我們需要“駕馭”在同步之上。
為了滿足happens-before,這個需要結合“程式次序法則”與另外一種次序法則(通常是“監視器鎖法則或“volatile變數法則”)來對訪問變數的操作進行排序,否則就用鎖來保護它
對於上圖A操作happens-before 於B,C操作happens-before於D。因為count是volatile,所以對count的寫要happens-before於讀操作。所以B操作happens-before於C。
根據傳遞性,連線上面三個 happens-before 關係得到:A appens-before 於 B; B appens-before C;C happens-before D。也就是說:寫執行緒 M 對連結串列做的結構性修改,在讀執行緒 N 讀取了同一個 volatile 變數後,對執行緒 N 也是可見的了。
雖然執行緒 N 是在未加鎖的情況下訪問連結串列。Java 的記憶體模型可以保證:只要之前對連結串列做結構性修改操作的寫執行緒 M 在退出寫方法前寫 volatile 型變數 count,讀執行緒 N 在讀取這個 volatile 型變數 count 後,就一定能“看到”這些修改。
這個特性和前面介紹的 HashEntry 物件的不變性相結合,使得在 ConcurrentHashMap 中,讀執行緒在讀取雜湊表時,基本不需要加鎖就能成功獲得需要的值。這兩個特性相配合,不僅減少了請求同一個鎖的頻率(讀操作一般不需要加鎖就能夠成功獲得值),也減少了持有同一個鎖的時間(只有讀到 value 域的值為 null 時 , 讀執行緒才需要加鎖後重讀)。
這個設計非常精彩,要對JMM非常熟悉。跟jdk8的處理手法有異曲同工之妙。
其他的revove修改的操作跟putVal操作類似這裡就不做分析了。
參考
《java併發程式設計實戰》
http://ifeve.com/atomiclong-and-longadder/
http://brokendreams.iteye.com/blog/2259857
http://blog.csdn.net/u010723709/article/details/48007881
http://www.cnblogs.com/daxin/p/3366606.html
http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/
https://en.wikipedia.org/wiki/Compare-and-swap
http://www.cnblogs.com/Mainz/p/3546347.html
http://coolshell.cn/articles/9703.html
http://coolshell.cn/articles/8239.html