ConcurrentHashMap線程安全及實現(xiàn)原理實例解析
背景
- 線程不安全的HashMap
因為多線程環(huán)境下,使用Hashmap進行put操作會引起死循環(huán),導致CPU利用率接近100%(jdk1.7,1.8引入紅黑樹優(yōu)化了),多線程put可能會導致元素丟失。所以在并發(fā)情況下不能使用HashMap。
- 效率低下的HashTable容器
JDK1.7的實現(xiàn)
在JDK1.7版本中,ConcurrentHashMap的數(shù)據(jù)結構是由一個Segment數(shù)組和多個HashEntry組成,如下圖所示:

Segment數(shù)組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分段技術,而每一個Segment元素存儲的是HashEntry數(shù)組+鏈表,這個和HashMap的數(shù)據(jù)存儲結構一樣。Segment(桶)
應用場景
當有一個大數(shù)組時需要在多個線程共享時就可以考慮是否把它給分層多個節(jié)點了,避免大鎖。并可以考慮通過hash算法進行一些模塊定位。 其實不止用于線程,當設計數(shù)據(jù)表的事務時(事務某種意義上也是同步機制的體現(xiàn)),可以把一個表看成一個需要同步的數(shù)組,如果操作的表數(shù)據(jù)太多時就可以考慮事務分離了(這也是為什么要避免大表的出現(xiàn)),比如把數(shù)據(jù)進行字段拆分,水平分表等.
初始化
ConcurrentHashMap的初始化是會通過位與運算來初始化Segment的大小,用ssize來表示,如下所示
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}如上所示,因為ssize用位于運算來計算(ssize <<=1),所以Segment的大小取值都是以2的N次方,無關concurrencyLevel的取值,當然concurrencyLevel最大只能用16位的二進制來表示,即65536,換句話說,Segment的大小最多65536個,沒有指定concurrencyLevel元素初始化,Segment的大小ssize默認為16
每一個Segment元素下的HashEntry的初始化也是按照位于運算來計算,用cap來表示,如下所示
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;如上所示,HashEntry大小的計算也是2的N次方(cap <<=1), cap的初始值為1,所以HashEntry最小的容量為2
JDK1.8的實現(xiàn)
JDK1.8的實現(xiàn)已經摒棄了Segment的概念,而是直接用Node數(shù)組+鏈表+紅黑樹的數(shù)據(jù)結構來實現(xiàn),并發(fā)控制使用Synchronized和CAS來操作,整個看起來就像是優(yōu)化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數(shù)據(jù)結構,但是已經簡化了屬性,只是為了兼容舊版本。

基本屬性:
// 默認初始值,必須是2的幕數(shù) private static final int DEFAULT_CAPACITY = 16; // 負載因子 private static final float LOAD_FACTOR = 0.75f; // 鏈表轉紅黑樹閥值,> 8 鏈表轉換為紅黑樹 static final int TREEIFY_THRESHOLD = 8; //樹轉鏈表閥值,小于等于6 ,僅在擴容tranfer時才可能樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64; private static final int MIN_TRANSFER_STRIDE = 16; private static int RESIZE_STAMP_BITS = 16; // 2^15-1,help resize的最大線程數(shù) private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // 32-16=16,sizeCtl中記錄size大小的偏移量 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // forwarding nodes的hash值 static final int MOVED = -1; // 樹根節(jié)點的hash值 static final int TREEBIN = -2; // ReservationNode的hash值 static final int RESERVED = -3; // 可用處理器數(shù)量 static final int NCPU = Runtime.getRuntime().availableProcessors(); //存放node的數(shù)組 第一次插入數(shù)據(jù)的時候初始化,大小是2的冪次方 transient volatile Node<K,V>[] table; /*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義 *當為負數(shù)時:-1代表正在初始化,-N代表有N-1個線程正在 進行擴容 *當為0時:代表當時的table還沒有被初始化 *當為正數(shù)時:表示初始化或者下一次進行擴容的大小 ,類似于擴容閾值。它的值始終是當前ConcurrentHashMap容量的0.75倍,這與loadfactor是對應的。實際容量>=sizeCtl,則擴容。 private transient volatile int sizeCtl;
put操作
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //兩次hash,減少hash沖突,可以均勻分布
int binCount = 0;
for (Node<K,V>[] tab = table;;) { //對這個table進行迭代
Node<K,V> f; int n, i, fh;
//這里就是上面構造方法沒有進行初始化,在這里進行判斷,為null就調用initTable進行初始化,屬于懶漢模式初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置沒有數(shù)據(jù),就直接無鎖插入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//如果在進行擴容,則先進行擴容操作
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//如果以上條件都不滿足,那就要進行加鎖操作,也就是存在hash沖突,鎖住鏈表或者紅黑樹的頭結點
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { //表示該節(jié)點是鏈表結構
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//這里涉及到相同的key進行put就會覆蓋原先的value
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) { //如果鏈表的長度大于8時就會進行紅黑樹的轉換
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//統(tǒng)計size,并且檢查是否需要擴容
return null;
}這個put的過程很清晰,對當前的table進行無條件自循環(huán)直到put成功,可以分成以下六步流程來概述。
- 如果沒有初始化就先調用initTable()方法來進行初始化過程
- 如果沒有hash沖突就直接CAS插入
- 如果還在進行擴容操作就先進行擴容
- 如果存在hash沖突,就加鎖來保證線程安全,這里有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,
- 最后一個如果該鏈表的數(shù)量大于閾值8,就要先轉換成黑紅樹的結構,break再一次進入循環(huán)
- 如果添加成功就調用addCount()方法統(tǒng)計size,并且檢查是否需要擴容
現(xiàn)在我們來對每一步的細節(jié)進行源碼分析,在第一步中,符合條件會進行初始化操作,我們來看看initTable()方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {//空的table才能進入初始化操作
if ((sc = sizeCtl) < 0) //sizeCtl<0表示其他線程已經在初始化了或者擴容了,掛起當前線程
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//CAS操作SIZECTL為-1,表示初始化狀態(tài)
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//初始化
table = tab = nt;
sc = n - (n >>> 2);//記錄下次擴容的大小
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}在第二步中沒有hash沖突就直接調用Unsafe的方法CAS插入該元素,進入第三步如果容器正在擴容,則會調用helpTransfer()方法幫助擴容,現(xiàn)在我們跟進helpTransfer()方法看看
/**
*幫助從舊的table的元素復制到新的table中
*/
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) { //新的table nextTba已經存在前提下才能幫助擴容
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
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;
}其實helpTransfer()方法的目的就是調用多個工作線程一起幫助進行擴容,這樣的效率就會更高,而不是只有檢查到要擴容的那個線程進行擴容操作,其他線程就要等待擴容操作完成才能工作。
既然這里涉及到擴容的操作,我們也一起來看看擴容方法transfer()
擴容原理
該方法的執(zhí)行邏輯如下:
- 計算每個線程可以處理的桶區(qū)間。默認 16.
- 初始化臨時變量 nextTable,擴容 2 倍。
- 死循環(huán),計算下標。完成總體判斷。
如果桶內有數(shù)據(jù),同步轉移數(shù)據(jù)。通常會像鏈表拆成 2 份。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 將 length / 8 然后除以 CPU核心數(shù)。如果得到的結果小于 16,那么就使用 16。
// 這里的目的是讓每個 CPU 處理的桶一樣多,避免出現(xiàn)轉移任務不均勻的現(xiàn)象,如果桶較少的話,默認一個 CPU(一個線程)處理 16 個桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // stride 一步的距離
// 新的 table 尚未初始化
if (nextTab == null) { // initiating
try {
//構建一個nextTable對象,其容量為原來容量的兩倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 擴容失敗, sizeCtl 使用 int 最大值。
sizeCtl = Integer.MAX_VALUE;
return;
}
// 更新成員變量
nextTable = nextTab;
// 更新轉移下標,就是 老的 tab 的 length
transferIndex = n;
}
// 新 tab 的 length
int nextn = nextTab.length;
// 創(chuàng)建一個 fwd 節(jié)點,用于占位。當別的線程發(fā)現(xiàn)這個槽位中是 fwd 類型的節(jié)點,則跳過這個節(jié)點。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 當advance == true時,表明該節(jié)點已經處理過了
boolean advance = true;
// 完成狀態(tài),如果是 true,就結束此方法。
boolean finishing = false; // to ensure sweep before committing nextTab
// 死循環(huán),i 表示下標,bound 表示當前線程可以處理的當前桶區(qū)間最小下標
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 控制 --i ,遍歷原h(huán)ash表中的節(jié)點
while (advance) {
int nextIndex, nextBound;
// 對 i 減一,判斷是否大于等于 bound (正常情況下,如果大于 bound 不成立,說明該線程上次領取的任務已經完成了。那么,需要在下面繼續(xù)領取任務)
// 如果對 i 減一大于等于 bound(還需要繼續(xù)做任務),或者完成了,修改推進狀態(tài)為 false,不能推進了。任務成功后修改推進狀態(tài)為 true。
// 通常,第一次進入循環(huán),i-- 這個判斷會無法通過,從而走下面的 nextIndex 賦值操作(獲取最新的轉移下標)。其余情況都是:如果可以推進,將 i 減一,然后修改成不可推進。如果 i 對應的桶處理成功了,改成可以推進。
if (--i >= bound || finishing)
advance = false; // 這里設置 false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
// 這里的目的是:1. 當一個線程進入時,會選取最新的轉移下標。2. 當一個線程處理完自己的區(qū)間時,如果還有剩余區(qū)間的沒有別的線程處理。再次獲取區(qū)間。
else if ((nextIndex = transferIndex) <= 0) {
// 如果小于等于0,說明沒有區(qū)間了 ,i 改成 -1,推進狀態(tài)變成 false,不再推進,表示,擴容結束了,當前線程可以退出了
// 這個 -1 會在下面的 if 塊里判斷,從而進入完成狀態(tài)判斷
i = -1;
advance = false;
}
// CAS 修改 transferIndex,即 length - 區(qū)間值,留下剩余的區(qū)間值供后面的線程使用
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; // 這個值就是當前線程可以處理的最小當前區(qū)間最小下標
i = nextIndex - 1; // 初次對i 賦值,這個就是當前線程可以處理的當前區(qū)間的最大下標
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 已經完成所有節(jié)點復制了
if (finishing) {
nextTable = null;
table = nextTab; // table 指向nextTable
sizeCtl = (n << 1) - (n >>> 1); // sizeCtl閾值為原來的1.5倍
return; // 跳出死循環(huán),
}
// 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
}
}
// 遍歷的節(jié)點為null,則放入到ForwardingNode 指針節(jié)點
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// f.hash == -1 表示遍歷到了ForwardingNode節(jié)點,意味著該節(jié)點已經處理過了
// 這里是控制并發(fā)擴容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 節(jié)點加鎖
synchronized (f) {
// 節(jié)點復制工作
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// fh >= 0 ,表示為鏈表節(jié)點
if (fh >= 0) {
// 構造兩個鏈表 一個是原鏈表 另一個是原鏈表的反序排列
int runBit = fh & n;
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);
// 在table i 位置處插上ForwardingNode 表示該節(jié)點已經處理過了
setTabAt(tab, i, fwd);
// advance = true 可以執(zhí)行--i動作,遍歷節(jié)點
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;
}
}
// 擴容后樹節(jié)點個數(shù)若<=6,將樹轉鏈表
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;
}
}
}
}
}
}擴容過程有點復雜,這里主要涉及到多線程并發(fā)擴容,ForwardingNode的作用就是支持擴容操作,將已處理的節(jié)點和空節(jié)點置為ForwardingNode,并發(fā)處理時多個線程經過ForwardingNode就表示已經遍歷了,就往后遍歷,下圖是多線程合作擴容的過程:

介紹完擴容過程,我們再次回到put流程,在第四步中是向鏈表或者紅黑樹里加節(jié)點,到第五步,會調用treeifyBin()方法進行鏈表轉紅黑樹的過程。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//如果整個table的數(shù)量小于64,就擴容至原來的一倍,不轉紅黑樹了
//因為這個閾值擴容可以減少hash沖突,不必要去轉紅黑樹
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
//封裝成TreeNode
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//通過TreeBin對象對TreeNode轉換成紅黑樹
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}到第六步表示已經數(shù)據(jù)加入成功了,現(xiàn)在調用addCount()方法計算ConcurrentHashMap的size,在原來的基礎上加一,現(xiàn)在來看看addCount()方法。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//更新baseCount,table的數(shù)量,counterCells表示元素個數(shù)的變化
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//如果多個線程都在執(zhí)行,則CAS失敗,執(zhí)行fullAddCount,全部加入count
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//check>=0表示需要進行擴容操作
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) {
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);
}
//當前線程發(fā)起庫哦哦讓操作,nextTable=null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}put的流程現(xiàn)在已經分析完了,你可以從中發(fā)現(xiàn),他在并發(fā)處理中使用的是樂觀鎖,當有沖突的時候才進行并發(fā)處理,而且流程步驟很清晰,但是細節(jié)設計的很復雜,畢竟多線程的場景也復雜。
get操作
我們現(xiàn)在要回到開始的例子中,我們對個人信息進行了新增之后,我們要獲取所新增的信息,使用String name = map.get(“name”)獲取新增的name信息,現(xiàn)在我們依舊用debug的方式來分析下ConcurrentHashMap的獲取方法get()
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //計算兩次hash
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {//讀取首節(jié)點的Node元素
if ((eh = e.hash) == h) { //如果該節(jié)點就是首節(jié)點就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值為負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來
//查找,查找到就返回
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首節(jié)點也不是ForwardingNode,那就往下遍歷
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}ConcurrentHashMap的get操作的流程很簡單,也很清晰,可以分為三個步驟來描述
- 計算hash值,定位到該table索引位置,如果是首節(jié)點符合就返回
- 如果遇到擴容的時候,會調用標志正在擴容節(jié)點ForwardingNode的find方法,查找該節(jié)點,匹配就返回
- 以上都不符合的話,就往下遍歷節(jié)點,匹配就返回,否則最后就返回null
size操作
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a; //變化的數(shù)量
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}在JDK1.8版本中,對于size的計算,在擴容和addCount()方法就已經有處理了,JDK1.7是在調用size()方法才去計算,其實在并發(fā)集合中去計算size是沒有多大的意義的,因為size是實時在變的,只能計算某一刻的大小,但是某一刻太快了,人的感知是一個時間段,所以并不是很精確。
總結與思考
其實可以看出JDK1.8版本的ConcurrentHashMap的數(shù)據(jù)結構已經接近HashMap,相對而言,ConcurrentHashMap只是增加了同步的操作來控制并發(fā),從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹,相對而言,總結如下思考:
- JDK1.8的實現(xiàn)降低鎖的粒度,JDK1.7版本鎖的粒度是基于Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節(jié)點)
- JDK1.8版本的數(shù)據(jù)結構變得更加簡單,使得操作也更加清晰流暢,因為已經使用synchronized來進行同步,所以不需要分段鎖的概念,也就不需要Segment這種數(shù)據(jù)結構了,由于粒度的降低,實現(xiàn)的復雜度也增加了
- JDK1.8使用紅黑樹來優(yōu)化鏈表,基于長度很長的鏈表的遍歷是一個很漫長的過程,而紅黑樹的遍歷效率是很快的,代替一定閾值的鏈表,這樣形成一個最佳拍檔
- JDK1.8為什么使用內置鎖synchronized來代替重入鎖ReentrantLock,我覺得有以下幾點:
- 因為粒度降低了,在相對而言的低粒度加鎖方式,synchronized并不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能通過Condition來控制各個低粒度的邊界,更加的靈活,而在低粒度中,Condition的優(yōu)勢就沒有了
- JVM的開發(fā)團隊從來都沒有放棄synchronized,而且基于JVM的synchronized優(yōu)化空間更大,使用內嵌的關鍵字比使用API更加自然
- 減少內存開銷 :在大量的數(shù)據(jù)操作下,對于JVM的內存壓力,基于API的ReentrantLock會開銷更多的內存,雖然不是瓶頸,但是也是一個選擇依據(jù)
以上就是ConcurrentHashMap線程安全及實現(xiàn)原理實例解析的詳細內容,更多關于ConcurrentHashMap線程安全的資料請關注腳本之家其它相關文章!
相關文章
SpringBoo WebFlux+MongoDB實現(xiàn)非阻塞API過程
本文介紹了如何使用SpringBootWebFlux和MongoDB實現(xiàn)非阻塞API,通過響應式編程提高系統(tǒng)的吞吐量和響應性能,適用于高并發(fā)場景2025-12-12
java實現(xiàn)圖片上加文字水印(SpringMVC + Jsp)
這篇文章主要為大家詳細介紹了java實現(xiàn)在圖片上加文字水印的方法,水印可以是圖片或者文字,操作方便,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-05-05
kotlin java 混合代碼 maven 打包實現(xiàn)
這篇文章主要介紹了kotlin java 混合代碼 maven 打包實現(xiàn),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03

