map线程安全的集合
呃,我们比较常用的HashMap和LinkedHashMap都不是不是线程安全的, HashTable和Properties是线程安全的, 那为什么还要ConcurrentHashMap呢?
关键点就是在于找到hash表最小冲突的地方进行加锁,这也才能提高效率, HashTable和Preperties都是直接在方法上加锁,那么锁的粒度就是整个集合对象。这样合理吗? 想一下hash映射, 假设来20个线程一起并发, 刚刚好20个线程hash映射之后没有发生hash冲突,都到了不同的hash槽位上,你说这需要锁码?所以我们可以找到更小的粒度就是hash槽位, 因为只有线程之间产生hash冲突到同一个槽位的时候才会出现并发问题,所以我们需要对槽位进行加锁。
口嗨一下罢了, 里面还有很多牛逼的细节, 真正内部逻辑复杂的很,不仅需要hashMap的前置只是,还需要JUC的知识真的难。
节点类型
ConcurrentHashMap作为一个并发集合,多线程之间可见性问题必须保证,所以volatile 或者 cas 以及 unsafe.getObjectVolatile(),synchronized 都是保证可见性的基石
绝了,和HashMap相比,多了2到3个。。。
构造方法
构造方法有两个,有些问题,一个使用加载因子,一个使用位移,两个获取到的结果有些不一样
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) /*初始容量不合法*/
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY : //如果初始容量大于等于最大容量的一半,使用最大容量
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 容量调整
this.sizeCtl = cap; //初始化容量--table表的大小
}
sizeCtl这个时候代表的意思就是 table待会初始化时候的容量大小
initTable 初始化table表
多个线程put的时候发现如果table没有初始化,都会指向这个方法,所以这个方法是多线程执行的,所以需要保证线程安全性。 设计思路:cas竞争成功的去初始化,失败的就while循环判断直到table初始化完成,一直循环导致浪费cpu,所以可以使用Thread.yeild来(尝试)释放cpu的使用权。。
这里注意sizeCtl这个时候的意思,sizeCtl一开始被赋值了table初始化的容量,所以cas竞争的初始化标志就是那个线程吧sizeCtl修改成了-1,那个这个吸线程就去出初始化table表,别的线程就礼让cpu使用权
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {// 10个线程来,只有一个线程抢到了进行初始化,其他的线程等等着用whiel循环,只有table初始化完才能出去这个方法
if ((sc = sizeCtl) < 0) //如果sizeCtl小于0, 说明其它线程正在扩容,当前线程让出CPU执行权
Thread.yield(); // lost initialization race; just spin 禁止失败,自旋
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //多线程cas 竞争初始化table的资格
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //构造函数是否传入初始化容量,没有就使用默认的容量16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //初始化
table = tab = nt; //volatile的写
sc = n - (n >>> 2); //(3/4)*n = 0.75n 阈值 下一次table扩容的阈值,
}
} finally {
sizeCtl = sc; //sizeCtl的含义从 初始化table容量变成了下一次扩容的阈值, 这里也就释放了cas锁
}
break;
}
}
return tab;
}
spread 扰动函数
同样是为了是的散列均匀
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS; //高位和地位进行异或,HASH_BITS 是为了避免为负
}
putVal
思考:这个方法需要考虑那些问题?
1、哈希槽位的竞争 2、冲突之后的同步 3、table表初始化的时候进行等待
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一开始是null的,第一次put的时候进行初始化, table是volatiles, 每次循环读都能读到最新的, 为啥使用for循环,因为多线程竞争,失败了的要进行重试
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) //table表没有进行初始化
tab = initTable(); //初始化hash表,这个方法是多线程执行的,需要保证安全性
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //(n - 1) & hash 确定槽位
if (casTabAt(tab, i, null, //cas 竞争
new Node<K,V>(hash, key, value, null))) //cas槽位竞争 这里cas底层用lock指令前缀保证了可见性, 虽然数组里面的本身是没有可见性的
break; // no lock when adding to empty bin 竞争成功put成功了 直接break
}
else if ((fh = f.hash) == MOVED) //这个槽位的节点已经发生扩容 迁移状态
tab = helpTransfer(tab, f); //当前线程协助去迁移。(我的槽位迁移了,帮助槽位中的元素都全部迁移完成之后,我在进行put)
else { //发生冲突。槽位的节点不是迁移状态,可以安全进行地址链表法解决冲突(链表和红黑树)
V oldVal = null;
synchronized (f) { //对槽位进行加锁
if (tabAt(tab, i) == f) { //判断f指向的node节点是否还在槽位, 为啥需要判断 f是否还在槽位中, 比如当前线程在获取了f到对f加锁这个时间中,另外一个线程已经把f进行移除,但是当前线程还保留这个引用,所以需要在判断一下。
if (fh >= 0) { //正常节点 链表, 节点类型为Node
binCount = 1; //计数,循环从槽位第一个节点开始, 所以初始值为1
for (Node<K,V> e = f;; ++binCount) { //替换或者塞入末尾
K ek;
if (e.hash == hash && //hash相等
((ek = e.key) == key || //key相等
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) //onlyIfAbsent 仅当没有值的时候才赋值
e.val = value; //替换
break;
}
Node<K,V> pred = e; //pred 记住前驱
if ((e = e.next) == null) { //如果为null 说明已经到达了链表末尾,挂上去
pred.next = new Node<K,V>(hash, key,
value, null);
break;//binCount 不会计数新增的节点 binCount为当前新接入的node节点前面一共有多少个节点(包括了槽位)
}
}
}
else if (f instanceof TreeBin) { //说明槽位上的节点是红黑树的代理标识。即已经进行了树化
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) { //寻找key相等的红黑树节点
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) { //
if (binCount >= TREEIFY_THRESHOLD) //是否达到扩容阈值,达到扩容阈值,当前链表上一共有9个
treeifyBin(tab, i); //树化逻辑
if (oldVal != null)
return oldVal; //说明是替换逻辑,baseCount不需要加一
break;
}
}
}
addCount(1L, binCount); //计数扩容
return null;
}
每个判断条件失败都要去重新进行volatile table的读,保证了可见性。。。 思考个问题 加锁的同步代码块,没有包裹树化逻辑和addCount ,会不会有线程安全问题?
tabAt 获取元素
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); //这里为啥要用volatile因为数组里面的值需要可见性。组数加了volatile只是保证数组变量的可见性并不保证,数组元素里面的值可见性
}
treeifyBin 树化
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //如果数组容量小于64,还是优先扩容,本次不进行树化
tryPresize(n << 1); //扩容一倍, table的容量
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//tabAt能获取最新的槽位的值, hash>0 说明是正常的节点
synchronized (b) { //对槽位进行加锁
if (tabAt(tab, index) == b) { //判断是否被移除了
TreeNode<K,V> hd = null, tl = null; //tl为尾指针 hd为头指针
for (Node<K,V> e = b; e != null; e = e.next) { //把Node类型的单向链表,变成TreeNode的双向链表
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null); //节点类型转化成TreeNode
if ((p.prev = tl) == null) //双向链表 尾插法
hd = p;
else
tl.next = p;
tl = p; //指向尾巴
}
setTabAt(tab, index, new TreeBin<K,V>(hd)); //将双向链表转化为红黑树, 在TreeBin的构造方法里面转化为红黑树
}
}
}
}
}
不出意料为了当前线程扩容时候的安全, 对该hash槽位进行了加锁。在变成TreeNode的双向链表之后, 使用了TreeBin的构造方法生成红黑树。 TreeBin里面的成员属性有TreeNode类型的,正是用来指向红黑树的根节点, 然后把TreeBin对象 放入hash槽。
TreeBin
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null); //设置为-2,代表TreeBin是一个红黑树代理节点, 这里也就说明了如果hash值为-2,那么这个槽位就是TreeBin
this.first = b; //TreeBin不仅使用root记录了红黑树的根节点,还使用first记录了双向链表的头指针
TreeNode<K,V> r = null; //r 根节点
for (TreeNode<K,V> x = b, next; x != null; x = next){ //从双向链表里面拿出一个节点
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null; //根节点的父亲节点为null
x.red = false; // 红黑树的根节点为 黑色。
r = x; //r 记录根节点
}
else { //红黑树的put方法
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) { //这里就是从根节点开始比较找到叶子节点
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<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) { //p开始移动
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x); //插入之后要进行平衡
break;
}
}
}
}
this.root = r; //红黑树的根节点
assert checkInvariants(root); //检查红黑树的性质
}
插入之后保持黑色平衡就不看了,和hashMap的一样。。。
putTreeVal
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) { //根节点
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h) //往左走
dir = -1;
else if (ph < h) //往右走
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk))) //找到
return p; //不需要插入
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) { //使用比较器比较
if (!searched) { //只用查一次就行
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk); //比较
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) { //找到最底层
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp); //创建节点
if (f != null)
f.prev = x; //头插入双向链表
if (dir <= 0) //挂左边
xp.left = x;
else //挂右边
xp.right = x;
if (!xp.red) //如果父亲节点是黑色, 不用做调整
x.red = true;
else { //父亲节点是红色
lockRoot(); //加寄生读写锁(寄生在Synchronized上), TreeBin里面不用加锁是因为,TreeBin是调整之后才给Root赋值
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot(); //
}
}
break;
}
}
assert checkInvariants(root); //检查是否符合红黑树性质
return null;
}
这部分代码和 HashMap的差不多, 但是 思考一下,调用putTreeVal外围不是已经对槽位进行加锁了,那为啥还要进行 黑色平衡的前后加上lockRoo和unLockRoot,原因就获取读的时候与写不进行互斥,原因就是在于调整的时候会对红黑树的parent和left right进行调整,如果这时候读还是走红黑树的查询,那坑定会有问题,但是别忘记了TreeBin 的first 还维护了一个TreeNode的双向链表结构,所以有写线程的时候,依旧可以读,只不过说读效率没有走红黑树那么块
TreeBin结构
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter; //写锁等待者
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock 持有写锁
static final int WAITER = 2; // set when waiting for write lock 等待写锁
static final int READER = 4; // increment value for setting read lock 读锁
}
TreeBin的lockState会表示当前槽位的状态,put的时候首先会在槽位加synchronized锁,所以写线程只能有一个, 但是读的话 是不用在槽位加synchronized的, 读的时候判断TreeBin的lockState如果没有写锁或则和等待写锁的 那么直接cas修改lockState为 读锁, 同样黑色平衡调整的时候判断lockState的状态是不是 读锁,如果读锁那么久进行park, 不是那就尝试把lockState修改成读锁
lockRoot
private final void lockRoot() {
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER)) //获取写锁失败 这里是和读线程竞争
contendedLock(); // offload to separate method
}
contendedLock
private final void contendedLock() { //lockState 的状态 0001 是写锁 0010 是等待获取写锁 0100 有别的线程持有读锁
boolean waiting = false; // 本方法是写锁线程是串行执行(synchronized 槽位) lockState的组合类型有 0001, 0110, 0100,0010
for (int s;;) {
if (((s = lockState) & ~WAITER) == 0) {//没有读锁或者写锁 0010
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) { //拿到写锁 0001
if (waiting)
waiter = null; //当前线程已经获取到写锁 ,清除等待标记
return;
}
}
else if ((s & WAITER) == 0) { //说明读锁被占了, 我需给lockState加上 等待标志
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) { // 0110
waiting = true;
waiter = Thread.currentThread();
}
}
else if (waiting) // 别人持有读锁了,写线程挂起, 读线程会把自己唤醒
LockSupport.park(this);
}
}
unlockRoot
private final void unlockRoot() {
lockState = 0;
}
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) { //hash槽位上有值
if ((eh = e.hash) == h) { //直接进行判断
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) //不是正常节点 (TreeBin 红黑树上去查找 或者 ForwardingNode 转发到nextTable上去查找 或者 reservationNode 占位符 直接放回null)
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;
}
find (TreeBin)
final Node<K,V> find(int h, Object k) { //多线程执行
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
if (((s = lockState) & (WAITER|WRITER)) != 0) { //这里是判断有没有写线程持有写锁,或者准备等待获取写锁, 那么我就不适用红黑树继续查找,而是用双向链表进行查找
if (e.hash == h && //写线程在等待的时候可能进入park, 这里发现是等待状态,就不去走红黑树查询
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) { //加读锁 每个读锁都会加4
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null)); //根节点开始找
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) == //-4 也就是减少一个读锁
(READER|WAITER) && (w = waiter) != null) //之前的状态是一个读锁加写锁等待
LockSupport.unpark(w); //释放了所有的读锁,那就去唤醒等待的线程
}
return p;
}
}
}
return null;
}
addCount 计数+扩容
累加计数这块设计和LongAddr一样,借助多核的优势,通过空间换时间,提高效率。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null || //计数器数组不为null
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { //直接在baseCount上加,cas失败,表示竞争激烈
CounterCell a; long v; int m;
boolean uncontended = true; //没有竞争
if (as == null || (m = as.length - 1) < 0 || //如果没有初始化,或者初始化了长度为0
(a = as[ThreadLocalRandom.getProbe() & m]) == null || //计数器槽位没有值
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { //在该计数器槽位进行cas竞争失败
fullAddCount(x, uncontended); //竞争失败 对于LongAdderd 的 longAccumulate
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) { //达到阈值并且数组不为空并且table的长度小于最大容量
int rs = resizeStamp(n); //根据原table的容量获取一个扩容的邮戳? 容量n转化为二进制后左边0的个数加上2^15
if (sc < 0) { //说明此时正在扩容 (sc >>> RESIZE_STAMP_SHIFT) != rs 表示有人已经扩容了 即table的长度已经发生改变
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || //sc==rs+1 这里是BUG jdk8的 正确写法 sc==rs<<RESIZE_STAMP_SHIFT + 1 , 说明当前只有最后一个线程了(第一个线程设置的线程数量为2,别的参与扩容都是加1),迁移已经完成了
sc == rs + MAX_RESIZERS || (nt = nextTable) == null || //sc == rs + MAX_RESIZERS => sc==rs<<RESIZE_STAMP_SHIFT+MAX_RESIZERS 可以帮吗扩容的线程数量已经达到最大了 nextTable为null 扩容也结束了
transferIndex <= 0) //迁移的时候 hash槽下标从末尾开始, transferIndex <=0 说明所有的hash槽位已经都被别人分配去扩容了
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) //参与扩容
transfer(tab, nt);
} //此时还没有扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc, //rs 低16位为1, 所以左移16位之后一定是负数
(rs << RESIZE_STAMP_SHIFT) + 2)) //sc>0 , 说明当前table还没有扩容, 这个时候sizeCtl含义 高16位为邮戳,低16为线程数量, 但是为啥加2??
transfer(tab, null); //开始扩容, 迁移
s = sumCount(); //获取总的计数
}
}
}
从代码中可以看出不是所有的线程都会参与扩容
sizeCtl从容量的含义变成了邮戳号(高16位)+扩容线程数了(低16位)
transfer
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride; //stride 线程处理的hash槽位数
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //NCPU cpu的核心数
stride = MIN_TRANSFER_STRIDE; // subdivide range 最小的处理hash槽位数量为16
if (nextTab == null) { // initiating 初始化
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容容量为原来的2倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; //volatile的写
transferIndex = n; //从原table末尾开始迁移
}
int nextn = nextTab.length; //新table长度
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //这里的ForwardingNode里面持有nextTable, 槽位赋值整个类型的,hash值为-1,代表正在扩容迁移, 每个线程都会创建一个
boolean advance = true; //该标识代表要不要处理下一个hash桶
boolean finishing = false; // to ensure sweep before committing nextTab 控制扩容何时结束
for (int i = 0, bound = 0;;) { //开始处理hash桶 [开头, 结尾]
Node<K,V> f; int fh;
while (advance) { //分配任务
int nextIndex, nextBound;
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 ? //剩余的槽位还比strde(最小分配槽位)小的话
nextIndex - stride : 0))) { //cas竞争 分配任务
bound = nextBound;
i = nextIndex - 1; //往前推荐槽位
advance = false;
}
}//任务分配完成,开始迁移
if (i < 0 || i >= n || i + n >= nextn) { //i<0 说明 当前线程不需要帮助迁移槽位了,任务完成了 i>=n 和 i+n >= nextn 都是技术上的判断一个是原数组不会越界,另外一个是扩容二倍一定大于i+n
int sc;
if (finishing) { //结束了 --最后一个收尾线程能进入这个逻辑
nextTable = null; //扩容完成
table = nextTab; //table指向新的table表, 原table上的东西 能被GC回收
sizeCtl = (n << 1) - (n >>> 1); //新的阈值设置 2*n - n/2 = 3/2n =》 2*n*0.75=2*(3/4)*n 等价的
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { //当前线程退出,帮助扩容的线程的数量减少1
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) //判断是不是最后一个线程
return; //不是最后一个线程 直接结束
finishing = advance = true; //最后一个线程去做完收尾工作就结束
i = n; // recheck before commit 这个时候 bound是0, i为原table长度,所以最后一个线程从尾到头进行扫描一次。
}
}
else if ((f = tabAt(tab, i)) == null) //槽位为null
advance = casTabAt(tab, i, null, fwd); //一个线程共用fwd 这里是和put方法的线程进行竞争
else if ((fh = f.hash) == MOVED) //已经迁移了
advance = true; // already processed
else { //需要进行迁移(链表或者红黑树)
synchronized (f) { //加锁
if (tabAt(tab, i) == f) { //判断是不是被移除了 这里和hashmap的思想一样都是分为高桶和低桶
Node<K,V> ln, hn;
if (fh >= 0) { // 槽位上是链表结构, 整体上的算法和hashMap有点不一样
int runBit = fh & n;//如果hash值大于长度说明是要去高桶
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n; //是否大于原长度,0就是在低桶, 大于1就是在高桶
if (b != runBit) { //说明f和下一个节点是要分别分配到高位和低位
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); //呃,原table现在还没有清楚,还在迁移过程中,但是i这个槽位已经迁移完成了, 所以需要一个转发节点,去新的table上去找
advance = true; //该槽位迁移完成
}
else if (f instanceof TreeBin) { //如果是红黑树代理点 也和hashMap差不多一样,使用树节点的双向链表结构。。
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) { //first指针指向的就是双向链表的头节点
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) : //反树化,从双向链表变成Node类型的单向链表
(hc != 0) ? new TreeBin<K,V>(lo) : t; //转化为红黑树, 这里有个技巧,如果原槽位上的节点要么都保留原槽位或者都去了高槽位,那么直接使用原槽位的treeBin就行了
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, 再槽位设计了转发几点,转发节点会指向扩容的新table, 等所有槽位都进行迁移完成之后,原table指向新table,多线程扩容小效率很高, 呃, 迁移的时候设计到链表和红黑树节点的改变所以,需要对新table的槽位进行加锁,避免别的线程通过原table表的槽位(这个槽位上是转发节点,通过转发节点可以找到新table)找到新table进行put,导致出现线程安全问题。
helpTransfer
B线程再对原table扩容的时候,另外一个线程A进行了put操作,如果key对应原table的槽位的hash值是-2,也就是这个槽位是转发节点,代表这个槽位已经迁移到新table上了,所以另外一个线程A可以去协助B进行扩容
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) && //原table不为null,并且槽位上的节点是转发节点
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //新table不为null
int rs = resizeStamp(tab.length); //获取一个邮戳和原tablec长度有关
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) { //正在扩容中
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0) //这些条件和addCount 里面的一样
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab); //扩容
break;
}
}
return nextTab;
}
return table;//返回最新的table值, volatile
}
tryPresize
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : //size大于最大容量的一半 就使用最大容量
tableSizeFor(size + (size >>> 1) + 1); //获取容量
int sc;
while ((sc = sizeCtl) >= 0) { //说明table还没有扩容
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) { //table还没有初始化
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //进行初始化扩容
try {
if (table == tab) { //说明table没有别的线程扩容
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //扩容
table = nt; //指向新table
sc = n - (n >>> 2); //新的阈值
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY) //下于现有容量或者大于最大容量,不需要扩容
break;
else if (tab == table) { //还没有扩容完成
int rs = resizeStamp(n);
if (sc < 0) { //扩容已经开始了则加入
Node<K,V>[] nt;
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);
}
}
}
replaceNode
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode()); //hash映射
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null) //不存在
break;
else if ((fh = f.hash) == MOVED) //说明table再扩容, 需要协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) { //加锁
if (tabAt(tab, i) == f) { //没有被remove
if (fh >= 0) { //链表
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { //是否相等
V ev = e.val; //保留旧值
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev; //保存旧值
if (value != null) //新value不为null
e.val = value; //替换
else if (pred != null) //前驱为不空, 越过该节点
pred.next = e.next;
else //前驱问空,把next放入槽位
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) { //红黑树
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) { //从根节点开始找
V pv = p.val; //记录老值
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null) //替换
p.val = value;
else if (t.removeTreeNode(p)) //删除 返回值是否要反树化
setTabAt(tab, i, untreeify(t.first)); //反树化
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1); //删除只是计数,不进行扩容
return oldVal;
}
break;
}
}
}
return null;
}
removeTreeNode
首先进行双向链表的删除, 然后进行红黑树的删除, 呃,HashMap和ConcurrentHashMap的删除逻辑差不多,区别就是ConcurrentHashMap再对红黑树删除操作的时候需要加读锁
final boolean removeTreeNode(TreeNode<K,V> p) {
TreeNode<K,V> next = (TreeNode<K,V>)p.next;
TreeNode<K,V> pred = p.prev; // unlink traversal pointers
TreeNode<K,V> r, rl;
if (pred == null) /*双向链表的删除*/
first = next;
else
pred.next = next;
if (next != null)
next.prev = pred;
if (first == null) {
root = null;
return true;
}
if ((r = root) == null || r.right == null || // too small
(rl = r.left) == null || rl.left == null)
return true;
lockRoot();//红黑树的删除 加寄生写锁
try {
TreeNode<K,V> replacement;
TreeNode<K,V> pl = p.left;
TreeNode<K,V> pr = p.right;
if (pl != null && pr != null) { //删除节点的左右孩子都不为null
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor 得到中序遍历的后继
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors 交互颜色
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent p的右子树没有左子树
p.parent = s; //对调
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) { //p指向s的parent
if (s == sp.left) //p替换了s的位置
sp.left = p;
else //兼容 前前驱的情况
sp.right = p;
}
if ((s.right = pr) != null)//s替代p都位置
pr.parent = s;
}
p.left = null; //p左孩子为null
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null) // 根节点
r = s;
else if (p == pp.left) /*删除非空节点*/
pp.left = s;
else
pp.right = s;
if (sr != null) //位置调整之后p有孩子
replacement = sr;
else //没有孩子
replacement = p;
}
else if (pl != null) //删除点击有孩子
replacement = pl;
else if (pr != null) //删除节点有孩子
replacement = pr;
else //删除节点没有孩子
replacement = p;
if (replacement != p) { //说明删除节点有后继
TreeNode<K,V> pp = replacement.parent = p.parent; //替换节点代替 删除节点
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null; //删除节点 GC
}
root = (p.red) ? r : balanceDeletion(r, replacement); //替代节点平衡调整
if (p == replacement) { // detach pointers 删除节点没有左右孩子, 删除调替代节点
TreeNode<K,V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
unlockRoot(); //解锁
}
assert checkInvariants(root);
return false;
}
删除之后调整平衡和HashMap的一样
ConcurrentHashMap的删除和替换都是调用的replaceNode
compute
需求 K确定好了,但是V还没有确定好, 所以先占位
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) { //v为函数式接口
if (key == null || remappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode()); //hash映射
V val = null;
int delta = 0;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) //初始化table表
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) { //槽位上没有
Node<K,V> r = new ReservationNode<K,V>(); //占位节点
synchronized (r) { //类似局部变量???
if (casTabAt(tab, i, null, r)) { //cas 把占位符塞入槽位 如果成功,别的线程可以看到槽位上的占位符
binCount = 1;
Node<K,V> node = null;
try {
if ((val = remappingFunction.apply(key, null)) != null) { //计算value
delta = 1;
node = new Node<K,V>(h, key, val, null); //创建Node
}
} finally {
setTabAt(tab, i, node); //替换占位符
}
}
}
if (binCount != 0) //说明计算得到结果了
break;
}
else if ((fh = f.hash) == MOVED) //扩容中
tab = helpTransfer(tab, f); //协助
else {
synchronized (f) { //加锁
if (tabAt(tab, i) == f) { //没有被删除
if (fh >= 0) { //链表 新增 、 删除 、 修改 都包括
binCount = 1;
for (Node<K,V> e = f, pred = null;; ++binCount) {
K ek;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { //找到相同key的
val = remappingFunction.apply(key, e.val); //计算新value
if (val != null) //新value不为空 进行替换
e.val = val;
else { //新value为null , 删除操作
delta = -1;
Node<K,V> en = e.next;
if (pred != null)
pred.next = en;
else
setTabAt(tab, i, en);
}
break;
}
pred = e;
if ((e = e.next) == null) { //链表上没有相等的key
val = remappingFunction.apply(key, null);
if (val != null) { //新增
delta = 1;
pred.next =
new Node<K,V>(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) { //红黑树
binCount = 1;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null) //有根节点
p = r.findTreeNode(h, key, null); //开始找到
else
p = null;
V pv = (p == null) ? null : p.val;
val = remappingFunction.apply(key, pv); //计算新value
if (val != null) {
if (p != null) //替换
p.val = val;
else { //新增
delta = 1;
t.putTreeVal(h, key, val);
}
}
else if (p != null) { //删除
delta = -1;
if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first)); //反树化
}
}
}
}
if (binCount != 0) { //装树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
break;
}
}
}
if (delta != 0) //要么新增节点了, 要么删除节点了
addCount((long)delta, binCount);
return val;
}
clear
public void clear() { //删除, 单向链表和双向链表
long delta = 0L; // negative number of deletions
int i = 0; //从左到右进行删除
Node<K,V>[] tab = table;
while (tab != null && i < tab.length) { //table表合规
int fh;
Node<K,V> f = tabAt(tab, i);//获取槽位的节点
if (f == null) //如果是null,就去下一个槽位
++i;
else if ((fh = f.hash) == MOVED) { //table正在扩容
tab = helpTransfer(tab, f); //协助扩容
i = 0; // restart 从头重新开始删除
}
else { //正常节点
synchronized (f) { //对槽位进行加锁
if (tabAt(tab, i) == f) { //如果槽位上的节点没有发生改变
Node<K,V> p = (fh >= 0 ? f :
(f instanceof TreeBin) ?
((TreeBin<K,V>)f).first : null); //是树节点还是正常链表节点
while (p != null) { //开始删除
--delta;
p = p.next;
}
setTabAt(tab, i++, null); //槽位置为null
}
}
}
}
if (delta != 0L)
addCount(delta, -1); //只进行计数, 不进行扩容
}
删除直接拿链表删除就行,不需要管红黑树