JDK1.8之前的HashMap采用的是数组和链表结合使用,也就是链表散列。HashMap 使用 key 的 hashcode 经过扰动函数处理过后得到hash值,然后通过 (n-1)&hash 得到当前元素存放的数组索引(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
扰动函数就是HashMap的hash方法,使用hash方法的目的就是防止一些实现性较差的hashCode方法,就是为了减少hash碰撞
// JDK1.7的hash方法
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// JDK1.8的hash方法
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
减少hash碰撞的方法
1、开放寻址法
基本思想:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
公式
Hi = (H(key) + di) MOD m, i=1,2,…,k(k<=m-1)
H(key)为hash函数,m为表长,di为增量序列。增量序列的取值方式不同,相应的再散列方式也不同
线性探测法:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表
di = 1,2,3,4,5,6,...,n
二次探测法:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
di = 1^2, -1^2, 2^2, -2^2, ....., k^2, -k^2
伪随机探测法:应建立一个伪随机数发生器,并给定一个随机数做起点
di = 伪随机序列
缺点:
- 容易产生数据堆积,不适用于大规模的数据存储
- 插入操作可能会出现多次冲突
- 删除冲突元素时,需要对后面的元素作处理,实现较复杂
- 节点规模很大时会浪费很多空间
2、二次hash(rehash)
这种方法是同时构造多个不同的hash函数
Hi = RHi(key), i=1,2,…,k
当 i=1 时取得的地址冲突时,会继续计算 i=2 时的地址,直到找到不冲突的地址为止。
这种方法不易产生聚集,但增加了计算时间
3、链地址法
为每一个hash值建立一个单链表,当发生冲突时,将记录插入到链表中
优点:
- 处理冲突简单,无堆积现象
- 各链表上的结点空间是动态申请的,它更适合于造表前无法确定表长的情况
- 拉链法构造的散列表中,删除结点的操作易于实现
4、建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
HashMap
JDK1.7采用拉链法设计HashMap
JDK1.8的HashMap在1.7的基础上增加了链表转红黑树,当某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD= 8),HashMap会动态的使用一个专门的TreeMap实现来替换掉它。提高查询效率,时间复杂度由O(n)减少为O(logn)
HashMap线程不安全的原因
1、多线程同时put可能导致元素丢失
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0){
// 初始化
n = (tab = resize()).length;
}
// 多线程的情况下,如果扩容进行到一半,在这里添加到新table的数据会被扩容再次hash的数据覆盖
// put成功但是get的时候是null
// 多线程多个hash值相同的进到这里判null,只有最后一个key成功
if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
} else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
}else if (p instanceof TreeNode){
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
for (int binCount = 0; ; ++binCount) {
// 这里也会导致数据丢失
if ((e = p.next) == null) {
// 如果一个线程执行到这里挂起,并且另一个线程正在扩容,扩容完成之后这里执行会导致内存泄露,通过get方法无法获取到这个node
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
e.value = value;
}
return oldValue;
}
}
// 多线程导致计数错误
++modCount;
if (++size > threshold)
resize();
return null;
}
jdk1.7的HashMap并发下扩容可能形成环状链表,导致get操作时,CPU空转
// HashMap#getNode
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
形成环状链表之后,如果查询环状链表中不存在的值就会一直循环。
jdk1.7的HashMap因为会将新添加的节点放在头节点所以会导致死循环
JDK1.8的HashMap已经修复了JDK1.7中插入节点的死循环问题,但是存在红黑树的死循环问题(多线程下操作同一对象时,对象内部属性的不一致性导致的)
JDK1.8死循环参考:https://blog.csdn.net/gs_albb/article/details/88091808
public class HashMap8 {
public static void main(String[] args) {
HashMapThread hmt0 = new HashMapThread();
HashMapThread hmt1 = new HashMapThread();
HashMapThread hmt2 = new HashMapThread();
HashMapThread hmt3 = new HashMapThread();
HashMapThread hmt4 = new HashMapThread();
HashMapThread hmt5 = new HashMapThread();
HashMapThread hmt6 = new HashMapThread();
HashMapThread hmt7 = new HashMapThread();
HashMapThread hmt8 = new HashMapThread();
HashMapThread hmt9 = new HashMapThread();
HashMapThread hmt10 = new HashMapThread();
HashMapThread hmt11 = new HashMapThread();
HashMapThread hmt12 = new HashMapThread();
hmt0.start();
hmt1.start();
hmt2.start();
hmt3.start();
hmt4.start();
hmt5.start();
hmt6.start();
hmt7.start();
hmt8.start();
hmt9.start();
hmt10.start();
hmt11.start();
hmt12.start();
}
}
class HashMapThread extends Thread {
private AtomicInteger ai = new AtomicInteger(0);
private static Map map = new HashMap<>(1);
@Override
public void run() {
while (ai.get() < 5000) {
map.put(new Person(ai.get(), "name" + Thread.currentThread().getName()), ai.get());
ai.incrementAndGet();
}
System.out.println(Thread.currentThread().getName() + "执行结束完" + ai.get());
}
}
可能出现如下循环异常
死循环的两个地方
// HashMap#putVal -> putTreeVal
// thread-3去获取红黑树的root节点的时候,红黑树在并发下死循环了
final TreeNode root() {
// 多线程死循环
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null){
return r;
}
r = p;
}
}
// HashMap#putVal -> treeifyBin
final void treeify(Node[] tab) {
for (TreeNode x = this, next; x != null; x = next) {
next = (TreeNode)x.next;
x.left = x.right = null;
if (root != null) {
K k = x.key;
int h = x.hash;
Class> kc = null;
// 多线程死循环
for (TreeNode p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h){
dir = -1;
}else if (ph < h){
dir = 1;
}else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0){
dir = tieBreakOrder(k, pk);
}
TreeNode xp = p;
// 红黑树循环导致find死循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
}
还有可能死循环的地方 java.util.HashMap.TreeNode#split 中的for循环,在resize 的时候死循环。
红黑树的死循环
ConcurrentHashMap
ConcurrentHashMap是线程安全的HashMap,有HashTable具有以下的区别
1、底层数据结构不同
JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
2、实现线程安全的方式
在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比:https://www.cnblogs.com/chengxiao/p/6842045.html
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
JDK1.8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
对于JDK1.8的ConcurrentHashMap主要分析一下几个方法
- putVal
- transfer
- size
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;
for (Node[] tab = table; ; ) {
Node f;
int n, i, fh;
if (tab == null || (n = tab.length) == 0) {
// 初始化
tab = initTable();
} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通过CAS去设置Node 存在空位置
if (casTabAt(tab, i, null, new Node(hash, key, value, null))) {
// 设置成功就直接返回
break; // no lock when adding to empty bin
}
} else if ((fh = f.hash) == MOVED) {
// 转发节点的hash标识
tab = helpTransfer(tab, f);
} else {
V oldVal = null;
// 把当前需要操作的node锁住
synchronized (f) {
// 再次检查是不是f
if (tabAt(tab, i) == f) {
// 正常单线程的插入流程
if (fh >= 0) {
// 在链表中插入
binCount = 1;
for (Node e = f; ; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 重复key直接替换 put -> onlyIfAbsent=false
if (!onlyIfAbsent) {
e.val = value;
}
break;
}
Node pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key, value, null);
break;
}
}
} else if (f instanceof TreeBin) {
// 在红黑树中插入 hash == -2
Node p;
binCount = 2;
if ((p = ((TreeBin) f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent) {
p.val = value;
}
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) {
// 检查是否需要转为红黑树
treeifyBin(tab, i);
}
// 这里return是因为集合的长度没有变化,可以直接return
if (oldVal != null) {
return oldVal;
}
break;
}
}
}
// binCount
// 0: 没有hash冲突
// 插入链表的位置index: 插入链表
// 2: 插入红黑树
// 修改cell的大小
addCount(1L, binCount);
return null;
}
addCount
// 从 putVal 传入的参数是 1, binCount size数据长度都是+1 x=1
private final void addCount(long x, int check) {
CounterCell[] as;
long b, s;
// 如果counterCells不是空,那么会使用counterCells来计数
// 如果counterCells空,在没有线程竞争的情况下使用baseCount voltile变量来计数
// 如果CAS的时候存在线程竞争,并且竞争失败的线程回去初始化 counterCells
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a;
long v;
int m;
boolean uncontended = true;
// as == null 表示并未出现并发
// 如果随机取余一个数组位置为空 或者 compareAndSwapLong(cellVlue) 失败 出现并发
if (as == null || (m = as.length - 1) < 0 ||
// 随机去一个值 & m
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// as[0] 或者 as[1] == null
(a = as[1 & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 初始化 counterCells
fullAddCount(x, uncontended);
// 这里初始化肯定没有到达扩容范围,因此可以直接return
return;
}
// 如果是单个node插入数组 check == 0
// 怎么才能到这个if => counterCells不是空 并且上面的if不成立
// 直接跳过的可能性是: 一个线程正在初始化counterCells,并且里面都存在值
// 但是又来了一个线程并且成功的设置了value = value + 1
// 这时就会跑到这里来,如果这里不是链表和红黑树就可以直接返回了
if (check <= 1) {
return;
}
// 算出插入元素之后的size 遍历counterCells计算出总元素个数
s = sumCount();
}
// 需要检查是否需要扩容
if (check >= 0) {
Node[] tab, nt;
int n, sc;
// 插入后的size >= sizeCtl(capacity)
while (s >= (long) (sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 根据 length 得到一个标识
// 如果n=16 前16位是 32 后16位 2^15 如果左移16位就是2^31 最小的负数
// 如果第一个线程已经在开始扩容,下一个线程过来这里还是resizeStamp(16)
int rs = resizeStamp(n);
// sc如果小于0说明正在扩容
if (sc < 0) {
// 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
// 如果 sc == 标识符 << Shift + 1 (扩容结束了,不再有线程进行扩容)
// (默认第一个线程设置 sc == rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs左移Shift + 1)
// 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
// 如果 nextTable == null(结束扩容了)
// 如果 transferIndex <= 0 (转移状态变化了)
// 结束循环
// rs => resizeStamp(tab.length)
// sc = sizeCtl = (rs << RESIZE_STAMP_SHIFT) + 2
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == (rs << RESIZE_STAMP_SHIFT) + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0) {
break;
}
// 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 扩容
transfer(tab, nt);
}
// sizeCtl = -2 表示有一个线程在进行扩容
} else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) {
// 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。
// 高 16 位是标识符,低 16 位初始是 2.
// 开始扩容
transfer(tab, null);
}
s = sumCount();
}
}
}
transfer
// 第一次扩容 nextTab = null
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride;
// thinkPad++ 8核
// 16 >>> 3 / cpu = 2/8 = 0
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) {
// subdivide range
stride = MIN_TRANSFER_STRIDE;
}
// 如果小于16 则取16 stride = 16
if (nextTab == null) { // initiating
try {
// << 1 扩大一倍
@SuppressWarnings("unchecked")
Node[] nt = (Node[]) new Node, ?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
// try to cope with OOME
// 这里就表示最大的容量可以是 MAX_VALUE = 2^31 - 1
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// 扩容之前的长度 比如:16
transferIndex = n;
}
// 新数组长度
int nextn = nextTab.length;
// 在新数组的头上增加一个需要转发的Node节点
ForwardingNode fwd = new ForwardingNode(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0; ; ) {
Node f;
int fh;
/**
* 多线程在这里会被指派不同区段的扩容任务,比如当前size=32时,需要扩容
* 线程A进行扩容 transferIndex = 16 i=31 bound=16 n=32 nextn=64 它负责的区间就是[16,31]
* 线程B进行扩容 transferIndex = 0 i=15 bound=0 它负责的区间就是[0,15]
* 因为存在 transferIndex 这volatile变量的限制,当没有任务给线程分配时,就会返回,将sizeCtl - 1
*/
while (advance) {
int nextIndex, nextBound;
// 这里的--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))) {
// 出现并发时这里只有一个线程可以成功 设置transferIndex的值为 nextBound 期望值是 nextIndex
bound = nextBound;
// transferIndex = 16 i=15
i = nextIndex - 1;
advance = false;
}
}
/**
* 如果当前的Map size=32
* 线程A进行扩容 transferIndex = 16 i=31 bound=16 n=32 nextn=64
* 线程B进行扩容 transferIndex = 0 i=15 bound=0
* 线程再次来进行扩容的时候,会进入第一个 else if,直接就退出了
* transferIndex = 0 i=-1 bound=0
*/
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 说明当前扩容已经完成,结束扩容逻辑并设置下一个扩容需要的阈值,这里并没有CAS去修改
// 什么样的线程可以到这里
// 1、进来扩容却没有拿到node节点 并且刚好在这里其他线程完成了扩容
// 2、这个finishing变量是一个局部变量,线程私有,只能是当前线程去把它设置为true
nextTable = null;
table = nextTab;
// 64 - 16 = 0.75*64 = 48 扩容阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 走到这里说明当前线程的扩容工作已经完成,需要将扩容线程数减1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT){
// 说明SC存在问题,当前的sizeCtl与table长度不对应
return;
}
finishing = advance = true;
// recheck before commit
i = n;
}
// i = 区间的最后一位
} else if ((f = tabAt(tab, i)) == null) {
// [0 ~ stride-1] [stride,stride*2-1]
// 如果区间的最后一位是null,则将扩容前的ForwardNode放入其中
advance = casTabAt(tab, i, null, fwd);
} else if ((fh = f.hash) == MOVED) {
// already processed
// 如果hash为MOVED,说明已经有线程在处理这一段扩容了
// 重新获取扩容段
advance = true;
} else {
// 把区间的最后一个节点锁住(最后一个节点在这里不为null),进行元素的移动
synchronized (f) {
// 再次检查
if (tabAt(tab, i) == f) {
Node ln, hn;
// 最后一个节点不是红黑树,可能是链表,也可能是单个的元素 => 使用链表遍历
if (fh >= 0) {
// 之前计算hash值是和 &(n-1) &n =0的不需要变位置 =1的新索引需要+n
// newIndex = hash & 1 1111
// oldIndex = hash & 0 1111
// 头节点的runBit
int runBit = fh & n;
Node lastRun = f;
for (Node p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
// 最后一个 b!=runBit 的节点p作为lastRun
lastRun = p;
}
}
if (runBit == 0) {
// 索引不变的链表
ln = lastRun;
hn = null;
} else {
// 索引+n的链表
hn = lastRun;
ln = null;
}
for (Node p = f; p != lastRun; p = p.next) {
int ph = p.hash;
K pk = p.key;
V pv = p.val;
// 构建两个新链表 新链表的顺序问题
if ((ph & n) == 0) {
// next = ln
// 新链表的顺序还是和原来的顺序一致
ln = new Node(ph, pk, pv, ln);
}else {
hn = new Node(ph, pk, pv, hn);
}
}
// ln链表放入新table的i位置
setTabAt(nextTab, i, ln);
// hn链表放入新table的i+n位置
setTabAt(nextTab, i + n, hn);
// 将需要转发的Node 放在区间的最后一个位置
setTabAt(tab, i, fwd);
// 再次进入循环
advance = true;
} else if (f instanceof TreeBin) {
// 区间的最后一个节点是红黑树,将红黑树移动到新table,将需要转发的Node 放在区间的最后一个位置
TreeBin t = (TreeBin) f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(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(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
fullAddCount
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
// 多线程下获取随机数,ThreadLocalRandom更适合用在多线程下,能大幅减少多线程并行下的性能开销和资源争抢
h = ThreadLocalRandom.getProbe();
wasUncontended = 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) {
// 计数器不是null,并且已经初始化完成
// 随机取出计数器中的cell
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x);
// Optimistic create
if (cellsBusy == 0 && U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// cellsBusy 设置为1 表示正在创建counterCell
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;
}
if (created)
break;
// 双重检测失败,证明其他线程在counterCells同一个位置建立了CounterCell
continue; // Slot is now non-empty
}
}
collide = false;
} else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 再次尝试去修改value
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
// 对counterCells进行扩容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
// cellsBusy置为1 表示正在创建cell
if (counterCells == as) {// Expand table unless stale
// 扩大为原来的两倍,最后continue
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 &&
// [A]
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// 初始化counterCells
// [B1]
boolean init = false;
try {
// Initialize table
if (counterCells == as) {
// 默认的长度时2
CounterCell[] rs = new CounterCell[2];
// 随机选一个 0 或者 1 来存储当前的cell
rs[h & 1] = new CounterCell(x);
counterCells = rs;
// [B2]
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
} else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
// 最后如果counterCells没有初始化并且在初始化counterCells的时候修改cellsBusy失败,
// 则会再次尝试去修改baseCount
// Fall back on using base
break;
}
}
- cellsBusy:1 表示 CounterCells 正在创建 ,0 表示其他状态。
- baseCount:用于统计 Map 中的 k-v 数,baseCount 用于在没有出现竞争的情况下统计。
- counterCells:数一个数组,每个数组项是一个 int,在 baseCount 上的递增出现竞争时会去取 counterCells 中的一个项进行递增,最终的 Map 中的 k-v 数总和是 baseCount 和 counterCells 所有计数之和,见下面的 subCount 方法。
- 三个地方都会去 CAS set cellsBusy,从 0 改成 1,并发下只有一个线程能进入临界区代码。临界区代码用 try finally 去保证 cellsBusy 最终一定会被设置回 0,相当于解锁。
- 如果 A 线程运行到「A」,另 B 线程运行到「B1」,如果 A 线程被挂起(比如 CPU 切换了执行线程),然而 B 线程继续执行,一直执行到了「B2」,这个时候 A 线程继续执行,如果没有 counterCells == as 判断,实惠重复创建 counterCells 的。这个是需要 counterCells 判断的理由。这个和并发下的单例设计模式一样,在进入锁以后需要重新判空一次。
- 这里的计数是使用了 counterCells,从源码的注释看,这个实现思路是和 java.util.concurrent.atomic.LongAdder 一致的。
- fullAddCount 方法的实现思路几乎和 LongAdder 一致。
remove
public V remove(Object key) {
return replaceNode(key, null, null);
}
// replaceNode和putVal方法类似,
// 链表替换的核心代码
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
// 找到了需要移除key
V ev = e.val;
if (cv == null || cv == ev || (ev != null && cv.equals(ev))) {
// 这里因为cv=null,所以后面都不需要判断
oldVal = ev;
// 修改前一个节点和后一个节点的指向
if (value != null){
e.val = value;
}else if (pred != null){
pred.next = e.next;
}else{
setTabAt(tab, i, e.next);
}
}
break;
}
// 红黑树的替换逻辑
if ((r = t.root) != null && (p = r.findTreeNode(hash, key, null)) != null) {
// 找到需要移除的节点P
V pv = p.val;
if (cv == null || cv == pv || (pv != null && cv.equals(pv))) {
// 修改p的value = null
oldVal = pv;
if (value != null){
p.val = value;
// TreeBin#removeTreeNode
}else if (t.removeTreeNode(p)){
setTabAt(tab, i, untreeify(t.first));
}
}
}
// 修改size
addCount(-1L, -1);
clear
public void clear() {
long delta = 0L; // negative number of deletions
int i = 0;
Node[] tab = table;
// 循环数组中的节点
while (tab != null && i < tab.length) {
int fh;
Node f = tabAt(tab, i);
if (f == null){
++i;
}else if ((fh = f.hash) == MOVED) {
// 节点状态是moved 说明正在扩展,则将当前线程也去参与扩容
// 扩容完成之后在clear
tab = helpTransfer(tab, f);
i = 0; // restart
} else {
// 锁住当前的node 防止put remove 扩容等操作
synchronized (f) {
if (tabAt(tab, i) == f) {
// hash值大于0说明是链表,返回头节点f
// 小于0 则返回红黑树的 first 节点
Node p = (fh >= 0 ? f :
(f instanceof TreeBin) ? ((TreeBin) f).first : null);
// 统计链表或者树的个数
while (p != null) {
--delta;
p = p.next;
}
// 把数组中当前的node 置为null
setTabAt(tab, i++, null);
}
}
}
}
if (delta != 0L){
// 修改size
addCount(delta, -1);
}
}