ConcurrentHashMap——设计本意
HashMap的线程安全问题:
(1)初始化的时候容易造成两个线程同时初始化 (2)扩容的时候,容易数字的实时性和判断代码的线程问题
(3)对于链表和红黑树的插入这种全局性影响结果的操作会产生线程安全,比如循环链表;
HashTable的性能低效问题:
hashTable直接所有线程共用一个把锁,put\get\resize串行执行。不同key之间也会造成竞争关系,性能太低
ConcurrentHashMap——版本对比
此图原文:https://blog.csdn.net/programmer_at/article/details/79715177
ConcurrentHashMap——适用场景
HashTable:强一致性;get\put串行执行;不适合大量的插入,以后可能不同key之间进行隔离也可以优化一下;
ConcurrentHashMap:弱一致性,需要减少哈希冲突;不然也会产生重量锁,链表和红黑树其实算局部性影响结果,所以可能以后版本升级会在这里优化一下;
ConcurrentHashMap——数据结构
nextTable属性
/**
* @迁移中转地
* 扩容时,将table中的元素迁移至nextTable;先扩容为两倍
* 扩容后,将nextTable清空
*/
private transient volatile Node<K,V>[] nextTable;
sizeCtl属性
/**
* @扩容状态标记/扩容阀值
* 这是一个联合意义的数据,可能作为状态标记;也可能作为阀值判断;
* 多线程之间,以volatile的方式读取sizeCtl属性,来判断ConcurrentHashMap当前所处的状态。通过cas设置sizeCtl属性,告知其他线程ConcurrentHashMap的状态变更
* 未初始化:sizeCtl=0:表示没有指定初始容量。sizeCtl>0:表示初始容量。
* 初始化中:sizeCtl=-1,标记作用,告知其他线程,正在初始化
* 正常状态:sizeCtl=0.75n ,扩容阈值
* 扩容中: sizeCtl < 0 : 表示有其他线程正在执行扩容;sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此时只有一个线程在执行扩容
*/
private transient volatile int sizeCtl;
transferIndex属性
/**
* 扩容线程每次最少要迁移16个hash桶
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* @分割迁移任务的数组指针
* 高并发清空下,多线程扩容会产生冲突;与其阻塞其他线程扩容,倒不如让其他线程协助扩容
* 因此首先将哈希按MIN_TRANSFER_STRIDE,16个哈希桶为单位分割为多个迁移任务
* transferIndex代表当前执行任务的位置,当然去获取扩容任务也会产生并发冲突所以用cas解决
* 扩容之前:transferIndex 在数组的最右边
* 扩容中:transferIndex=transferIndex-stride(stride是因此首先将哈希按MIN_TRANSFER_STRIDE的整数倍,代表可能不只一次一个单位)
* 由此可以看出迁移是从后往前迁移
*/
private transient volatile int transferIndex;
ForwardingNode内部类
标记作用,表示其他线程正在扩容,并且此节点已经扩容完毕
关联了nextTable,扩容期间可以通过find方法,访问已经迁移到了nextTable中的数据
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//hash值为MOVED(-1)的节点就是ForwardingNode
super(MOVED, null, null, null);
this.nextTable = tab;
}
//通过此方法,访问被迁移到nextTable中的数据
Node<K,V> find(int h, Object k) {
...
}
}
主要体现在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());
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;
}
/**
*@eh.hash<0,这样就可以在扩容期间访问已经迁移号的数据了
*/
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;
}
协助扩容源代码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
...
//f.hash == MOVED 表示为:ForwardingNode,说明其他线程正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
...
}
ConcurrentHashMap——插入元素
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;
for (Node<K,V>[] tab = table;;) {
/*
* tab 数组的引用
* n 数组的长度引用
* i 计算出来的哈希值
* f 每次找到的当前节点的遍历
* fh 当前节点的原哈希值
*/
Node<K,V> f; int n, i, fh;
/**
* @(1)数组为空就是懒加载,初始化一波
*/
if (tab == null || (n = tab.length) == 0)
tab = initTable();
/**
* @(2)表达当前这个位置是空的
* 直接利用cas更新节点,避免已经被插入了
*/
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
/**
* @(3)表示当前这个位置正在扩容
* 扩容操作在外部进行为了避免线程安全,会在此处先占位等待扩容完成后自己再加入
* 扩容好的节点链表或红黑树
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
/**
* @(4)表示当前这个位置是红黑树和链表
* 由于并发问题链表和红黑树是全局性会影响插入结果
* 因此插入过程中不能有最好不要有节点发生指针改变synchronized锁住
* 所以经常哈希冲突会影响性能
* binCount
*/
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
ConcurrentHashMap——获取数量
ConcurrentHashMap的元素个数等于baseCounter和数组里每个CounterCell的值之和,这样做的原因是,当多个线程同时执行CAS修改baseCount值,失败的线程会将值放到CounterCell中。所以统计元素个数时,要把baseCount和counterCells数组都考虑
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
ConcurrentHashMap——扩容机制
/**
* x,扩容一个
* check,检查原节点的基数(比如链表原来是地盘上个,单节点是1个,红黑树是节点个数)
*/
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/**
* @(1)并发基础size的处理
* 用于返回size以及各种扩容的判断基础
* BASECOUNT:偏移地址
* baseCount:期望值
* s:最终值等于原值+1
* counterCells as 更新的失败线程
*/
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
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))) {
/**
* @多线程修改baseCount时,用LongAdder方法实现数字并发,修改完后直接return整个方法
* 拆分成多个cell然后合并数字增加并发效率
* 最后也是直接return
*/
fullAddCount(x, uncontended);//
return;
}
/**
* @并发失败一次,如果只是单节点增加
* 也是直接return,否则就将并发修改的数量累加起来进行扩容判断
*/
if (check <= 1)
return;
s = sumCount();
}
/**
* @并发扩容的处理
* 没有baseCount的并发问题就进入扩容判断流程
*/
if (check >= 0) {
/**
* tab 数组指针
* nt 指向分段数组的指针
* n 数组的长度指针
* sc 扩容阀值
*/
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);
/**
* @sc<0表示需要进行并发扩容了
* 并发扩容时,nextTable可能还未必初始化或已经被初始化所以带个nt过来
*/
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);
}
/**
* @利用时间戳判断此时只有一个线程正在进行扩容
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
/**
* @n: 原table数组长度
* @nextTab: 是用来标识是不是第一个进来扩容的线程,是的话就需要初始化一下nextTale,由于volative是实时共享,但是我们只想知道移动前是不是null所以直接将指针复制进来
* @stride: 利用cpu空闲情况,算出此处线程应该应该拿到多个个任务
*/
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
/**
* @(1)为空就构建一个nextTable为table的两倍
*/
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;//将transferIndex等于数组长度
}
/**
* @构建一个死循环完成复制功能
*/
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false;
/**
* @nextn: 扩容的数组长度;
* @fwd: 将刚复制好的talbe包装一个ForwardingNode节点
* @advance:
* @finishing: 所有的节点都已经完成复制工作的标记
* @i: 每个线程的内置的计数器,用于判断自己的任务执行到哪里
* @bound: 每个线程的内置的哈希数量,用于判断自己的任务是否执行完
* f 扩容线程会根据transferIndex任务标记号,遍历拿到属于自己多个节点
* fh 每次遍历的节点的哈希
*/
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
/**
* @(1)主要将transferIndex同步给bound,然后不断i说明任务执行的进度
* 每个扩容线程先拿到任务号transferIndex
* 让并发的扩容线程获取到transferIndex拿到迁移任务
*/
while (advance) {
//更新迁移索引i。
int nextIndex, nextBound;
/**
* 执行完了标记需要直接退出循环
*/
if (--i >= bound || finishing)
advance = false;
/**
* transferIndex<=0表示已经没有需要迁移的hash桶,将i置为-1,线程准备退出
*/
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
/*
* 更新自己的bound尝试更新transferIndex
*/
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;
/**
* @如果已经完成复制
* 就将nextTable赋值为null,table指针指向我们的nextTab
* 然后扩容阀值增大
*/
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
/**
第一个扩容的线程,执行transfer方法之前,会设置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
后续帮其扩容的线程,执行transfer方法之前,会设置 sizeCtl = sizeCtl+1
每一个退出transfer的方法的线程,退出之前,会设置 sizeCtl = sizeCtl-1
那么最后一个线程退出时:
必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
*/
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
}
}
/**
* 空节点直接插入
*/
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;
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);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
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;
}
}
}
}
}
}
ConcurrentHashMap——关键梳理
分段锁的实现:jdk7是分割成二重数组后,用内置的lock锁实现;而jdk8是利用tranferIndex进行任务分割,每个关键锁只锁自己的任务哈希捅实现分段锁;
插入的时候:如果标记为Forward节点就帮其扩容,增加扩容效率;除此以外普通节点直接cas,红黑树和链表就是用关键字锁住自己的节点,锁的粒度更小
获取的时候:如果正在扩容可以利用Forward节点的find方法快速获取,增强一致性;
更新数量:如果并发更新因为争抢一个数组就会生成cell线程,类似于longadder的并发实现原理来合并最后的更新结果;
size的返回:所以线程的真实数量就是baseCount加上还在进行fullAddCount的线程的数量之和;
扩容机制:首先用nextTab判断null来决定初始化两倍的nextTab数组;然后用两个计数器吗,一个代表当前哈希迁移执行进度,一个代表获取到index边界;strcip任务总体大小依据cpu闲置情况判断,然后首先每个线程会不断进行cas获取到自己的分割任务的大小tranferIndex,然后同步给bound当然做了一些处理,最后就不断for循环+i,直到处理完就当前线程就完成扩容任务,可能会继续获取下一个任务,finshing用于判断所有节点是否迁移完;
GodSchool
致力于简洁的知识工程,输出高质量的知识产出,我们一起努力
博主私人微信:supperlzf