一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。 hashmap不安全,hashtable性能低,故而concurrent hashmap应运生。Java 7用分段锁,Java 8主要用CAS来保证并发安全。
Java 7 的实现
新的创新方式说采用Segment来实现,第一次哈希到segment,再次哈希到具体桶。Segment继承可重入锁,也就是每个分段一个锁,并发修改操作的时候先试自旋再加锁阻塞。 如下图所示:
- 1、a c的修改可以并发,不冲突
- 2、segment1 和segment 16的扩容相互独立
也就是说每个Segment的数组单独put/扩容,不会冲突。
稍加创新的Segment减小了put锁粒度,解决了hashtable并发效率低的问题。但是还不够完美,只是非常粗暴的分成N个段(比如16),每个段独自管理。 而Java8在处理ConcurrethashMap的时候做了非常精妙的设计,下面来看看。
Java 8 的优化
Java 8继续优化ConcurrethashMap,主要是采用CAS和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
- volatile的val和next
put
put的逻辑还算清晰(但仍需要足够的耐心和跳跃思维),下面看看put的具体实现,再总结其设计特点。
public V put(K key, V value) {
return putVal(key, value, false);
}
注意:下面将源码中的bin翻译为桶🪣
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 和hashtable一样,不支持null
if (key == null || value == null) throw new NullPointerException();
// 二次哈希,先不管
int hash = spread(key.hashCode());
//桶里面的节点个数
int binCount = 0;
//while循环,每次都重置为table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//tab得到初始化的表
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果这个hash所对应的桶为空,则CAS将这个key,value放到这个桶里
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // 如果CAS成功,这次put就愉快的结束了
//如果CAS失败,说明有其他抢先put这个桶成功了,则会继续跳转到 for (Node<K,V>[] tab = table;;) 执行
}
//f是桶的链表头,MOVED是-1,可是哈希值怎么会是-1?怎么不用sizeCtl值来判断这个是否合适,因为只需要帮忙某一个桶
else if ((fh = f.hash) == MOVED)//普通Node的哈希值保证不会为-1?
// 正在扩容中,那么先帮忙,tab被重新赋值为扩容后的表
tab = helpTransfer(tab, f);
//扩容完之后,则会继续跳转到 for (Node<K,V>[] tab = table;;) 执行
else {//命中的桶不为空
V oldVal = null;
synchronized (f) {//f是桶的链表头,同步锁住更新
if (tabAt(tab, i) == f) {//这个桶的初始值还没被更改
if (fh >= 0) {//不是特殊状态
binCount = 1;//在桶的链表的位置
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&//hash再次判断
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//找到e的key==key(基本类型),或对象用equals判断
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) {//红黑树的fh值=TREEBIN,是-2
//f是红黑树的根节点
Node<K,V> p;
binCount = 2;//为什么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;//返回old value
break;
}
}
}
addCount(1L, binCount);
return null;
}
看完这段代码,我们可以画出一个流程图,如下:
初始化表
初始化表的实现如下
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // 如果已经在初始化了,就让出cpu等待
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//CAS设置状态值
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//sc>0是什么场景?
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//分配内存
table = tab = nt;//table设置完
sc = n - (n >>> 2);//设置sc
}
} finally {
sizeCtl = sc;//设置扩容的阈值
}
break;//成功初始化
}
}
return tab;
}
-
sizeCtl<0表示是有可能在初始化中
- 负数表示在初始化或者扩容之中,-1初始化,-(1+n)扩容的线程数
-
不然CAS设置sizeCtl值为-1,开始初始化
-
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]分配内存 -
table=tab=nt
transient volatile Node<K,V>[] table;table也是volatile的,其他线程能马上看到。
-
sc=n-(n>>>2)的目的是用来设置扩容的阈值,n>>>2 无符号左移动两位,这个是除以4的意思
-
最后将sizeCtl的值设置为sc。这样初始化之后,sizeCtl的值为正数,表示下次扩容的阈值size。
private transient volatile int sizeCtl;可以看到sizeCtl也是volatile的,保证多线程可见性
-
最后返回一个tab
有个问题没有想清楚, 即使是第二次判断table==null之后,为什么这里finally要设置sizeCtl = sc?只是代码方便,还是有什么用意?不过有的时候只是一种保证不出错的写法,一定要问个究竟可能有的钻牛角尖。
-
Java8 扩容
put操作会判断,如果这个桶的头节点的hash已经是moved,那么put到这个hash桶的线程会来帮助扩容。 put之后,会判断是否超过阈值,如果超过则需要扩容。 这里还使用到了ForwardingNode,在扩容期间插入到链表头来帮助扩容到工具Node(哈希值设置为-1)
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
- nextTable是ForwardingNode的table,也就是扩容一倍的初始化好的新的table
helpTransfer
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) {
//resize标记
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//sc == rs + 1
//或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
//transferIndex <= 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;
}
(扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1) // 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
主要的实现是在transfer方法。
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
//通过计算 CPU 核心数和 Map 数组的长度得到每个线程(CPU)要帮助处理多少个桶
//并且这里每个线程处理都是平均的。
//默认每个线程处理 16 个桶。因此,如果长度是 16 的时候,扩容的时候只会有一个线程扩容
if (nextTab == null) { // 初始化
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//扩容两倍
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;//更新转移下标全局变量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 (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//advance表示正在进行推进的这样一个过程
while (advance) {
int nextIndex, nextBound;
//进入一个 while 循环,分配数组中一个桶的区间给线程,默认是 16.
//从大到小进行分配。当拿到分配值后,进行 i-- 递减。这个 i 就是数组下标。
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;//设置本线程的bound(分配一段空间),开始工作
i = nextIndex - 1;
advance = false;
}
}
//转移结束
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
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
}
}
//没什么可转移的,直接设置fwd标志
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; //已经被转移的情况,则不处理,如果两个线程并发transfer,[bound,transferIndex]范围是会存在一些重叠的区间的。
else {
synchronized (f) {//对桶的链表头加锁
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {//再次确认没有被转移,且不是红黑树
//对原来的hash和n(2^n)进行&运算, 结果只有两种情况,0和n
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
//对原来的hash和n(2^n)进行&运算, 结果只有两种情况,0和n
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变成一个链表
ln = new Node<K,V>(ph, pk, pv, ln);
else//头插法将hn变为一个链表
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 设置低位链表放在新链表的i
setTabAt(nextTab, i, ln);
//设置高位链表放在心链表的i+n
setTabAt(nextTab, i + n, hn);
//将旧链表设置为占位符
setTabAt(tab, i, fwd);
//继续向前推进
advance = true;
}
else if (f instanceof TreeBin) {//红黑树的情况t
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;
}
}
}
}
}
}
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;//链表头的key就已经等了
}
else if (eh < 0)//小于0则是红黑树,可能是红黑树
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//依次沿着链表往下找,如果是moved节点,可能是在nextTable里面寻找。
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
依次沿着链表往下找,如果是moved节点,可能是在nextTable里面寻找,这也不会影响get正确的获取到值