ConcurrentHashMap
ConcurrentHashMap基本概念
ConcurrentHashMap的数据结构是:数组+链表+红黑树
ConcurrentHashMap是线程安全的HashMap,通过使用CAS和Synchronized来实现线程安全
当出现hash冲突时,使用synchronized;未出现hash冲突时,使用CAS
在JDK1.7之前,使用的是HashTable,HashTable是通过在每个方法上使用synchronized来保证线程安全。在JDK1.7,使用的是ConcurrentHashMap,此时的ConcurrentHashMap使用的是segment来加锁。所谓segment,是将数组分成多个区域,这些区域叫做segment,每个区域共用一把锁。在JDK1.8,ConcurrentHashMap使用的是桶,所谓桶,是数组的每个索引位置,也就是说,数组的每个索引位置及其后面的链表或者红黑树共用一把锁。
ConcurrentHashMap之put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
put方法中会调用putVal方法,并且第三个参数默认传false
如果map中有相同key的值,那么put方法会覆盖原来的值,并且将原值返回
还有一种方法putIfAbsent
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
这个方法也是调用putVal方法,不过第三个参数默认传true
与put方法不同的是,如果map中有相同的key的值,那么此方法什么也不做,只将map中的值返回
putVal方法:
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 如果key为null或者value为null,抛出异常
if (key == null || value == null) throw new NullPointerException();
// 根据hashCode计算出hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f:新数据将要存放的数组索引位置上的旧数据
// n:数组长度
// i:新数据将要存放的数组索引位置
// fn:新数据将要存放的数组索引位置上的旧数据的hash值
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 数组还未初始化,将数组进行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 进入这里说明索引位置没有元素
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
// 将新元素成功放到数组中
break;
}
// 判断索引位置是否在扩容
else if ((fh = f.hash) == MOVED)
// 进入这里说明索引位置正在扩容
// 帮助进行扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 对当前索引位置的Node加锁
synchronized (f) {
// 再次判断索引位置的值是否改变,防止并发情况
if (tabAt(tab, i) == f) {
// 如果索引位置的Node的hash值小于0,此位置可能在扩容
if (fh >= 0) {
// 索引位置的Node的hash值大于等于0
// bitCount变量用于统计链表的遍历深度
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 进入这里说明索引位置的Node的key值与新元素的key值相同
oldVal = e.val;
// 如果onlyIfAbsent为true,啥也不做
// 如果onlyIfAbsent为false,新元素覆盖老元素
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) {
// 判断bitCount值是否大于8
if (binCount >= TREEIFY_THRESHOLD)
// 将链表转换成红黑树或者扩容
treeifyBin(tab, i);
if (oldVal != null)
// oldVal不为null,说明发生了覆盖的情况
// 返回旧值
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
spread方法:
static final int spread(int h) {
// 将key的hashCode值的高低16位进行异或运算,最终与HASH_BITS进行与运算
// 将高位的hashCode值也参与到计算索引位置的运算当中
// HashMap和ConcurrentHashMap要求数组长度是2的整次幂,为的也是让hashCode的更多位参与运算
// 减少hash冲突
// HASH_BITS的最高位是0,其他位是1,所以与HASH_BITS进行与运算,是保证hash值是正数
// 因为负数的hash值有特殊含义
// hash值为-1表示当前数组索引位置的数据正在扩容
// hash值为-2表示当前数组索引位置下挂载的是红黑树
// hash值为-3表示占用当前索引位置
return (h ^ (h >>> 16)) & HASH_BITS;
}
initTable方法:
// sizeCtl:是数组在初始化和扩容操作时的一个控制变量
// sizeCtl小于-1:低16位代表当前数组正在扩容的线程个数(如果有1个线程扩容,值为-2,依次类推)
// sizeCtl等于-1:代表当前数组正在初始化
// sizeCtl等于0:代表数组还未初始化
// sizeCtl大于0:代表当前数组的扩容阈值
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 判断当前数组是否初始化了
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// sizeCtl小于0,可能正在初始化
// 当前线程交出cpu资源
Thread.yield();
// 通过CAS,将sizeCtl值改成-1,代表正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再次判断数组是否初始化了
if ((tab = table) == null || tab.length == 0) {
// 如果sc大于0,代表数组扩容阈值
// 否则使用默认的数组容量(16)
// 如果在创建ConcurrentHashMap对象时,没有指定容量大小
// 那么此处n为16,如果指定了容量大小,那么就不是16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 生成新的数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算出数组下次扩容的阈值
sc = n - (n >>> 2);
}
} finally {
// 将扩容阈值赋值给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap的扩容方法
treeifyBin方法
// 当链表长度大于等于8时,执行此方法
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
// 判断数组是否初始化了
if (tab != null) {
// 判断数组容量是否小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 数组容量小于64,触发扩容
tryPresize(n << 1);
// 判断索引位置的元素不为null,并且hash值大于等于0,是正常的元素
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
// 判断索引位置的元素是否变化,避免并发
if (tabAt(tab, index) == b) {
// hd:双向链表的头节点
// tl:辅助变量,辅助生成双向链表
TreeNode<K,V> hd = null, tl = null;
// for循环生成双向链表
for (Node<K,V> e = b; e != null; e = e.next) {
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构造函数,生成红黑树
// 并且将红黑树放到数组中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
tryPresize方法
背景栏
ConcurrentHashMap的属性:
MAXIMUM_CAPACITY = 1 << 30
RESIZE_STAMP_BITS = 16
RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS
MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1
板书栏
// size是之前的数组长度乘以2得到的结果
private final void tryPresize(int size) {
// 如果扩容的长度达到最大值,就使用最大值
// 否则需要保证数组的长度为2的n次幂,tableSizeFor方法可以做到
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
// 将sizeCtl值赋值给sc,并且判断是否大于等于0
while ((sc = sizeCtl) >= 0) {
// 进入这里说明数组没有在初始化和扩容
Node<K,V>[] tab = table; int n;
// 判断数组是否已经初始化
// if条件满足的情况是使用了ConcurrentHashMap的入参是map的构造函数
if (tab == null || (n = tab.length) == 0) {
// 如果扩容阈值大于计算出来的扩容长度,使用扩容阈值,否则使用计算的扩容长度
// 当将一个map作为参数传给ConcurrentHashMap的构造函数时,n才有可能赋值为sizeCtl
// 此时sizeCtl值为16
n = (sc > c) ? sc : c;
// 通过CAS将sizeCtl改成-1
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 再次判断table没有变化,防止并发情况
if (table == tab) {
// 数组初始化
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
// 计算出新的扩容阈值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
// 进入这里说明计算的扩容长度小于等于扩容阈值(这种情况只有putAll方法才有可能)
// 或者扩容长度已经达到最大值,退出循环
break;
else if (tab == table) {
// 计算扩容标识戳,根据当前数组长度计算一个16位的扩容标识戳
// 第一个作用是为了保证后面的sizeCtl赋值时,sizeCtl是小于-1的负数
// 第二个作用是用来记录当前是从什么长度开始扩容
int rs = resizeStamp(n);
// 这里是一个BUG
// 下面的if条件永远不会满足,因为sc只在while循环条件语句中被赋值了
// 而且也做了大于等于0的判断
if (sc < 0) {
// 协助扩容的代码,但是进不来
Node<K,V>[] nt;
if (
// 当前线程扩容时,sc的高16位是rs的低16位左移16位得到的
// 应该与rs的低16位相同
// 不相同说明数组发生变化
(sc >>> RESIZE_STAMP_SHIFT) != rs
// 判断当前扩容是否已经结束
// 正确的写法应该是:sc == rs << 16 + 1
// BUG
|| sc == rs + 1
// 判断当前扩容的线程是否已经达到最大值
// 正确的写法应该是:sc == rs << 16 + MAX_RESIZERS
// BUG
|| sc == rs + MAX_RESIZERS
// 判断扩容是否已经结束
|| (nt = nextTable) == null
// transferIndex用来记录迁移的索引位置,从高位往低位迁移
// 判断扩容是否已经结束
|| transferIndex <= 0)
break;
// 如果线程需要协助扩容,首先要对sizeCtl进行+1操作,代表当前有一个线程协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 上面的判断没进去的话,nt代表新数组
transfer(tab, nt);
}
// 当前线程是第一个对数组进行扩容的线程,会对sizeCtl值进行特殊处理
// 将扩容标识戳左移16位后,符号位是1,代表负数
// 低16位表示当前扩容的线程有多少个,2就代表有1个
// 每一个线程扩容完毕,会对低16位-1操作,当最后一个线程扩容完毕,减一的结果是负一
// 当值为-1时,要对老数组进行一波扫描,查看是否有遗漏数据没有迁移
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 进入这里说明没有其他线程正在扩容
// 数组扩容
transfer(tab, null);
}
}
}
// 此方法返回一个低16位才有值的int类型数据
static final int resizeStamp(int n) {
// numberOfLeadingZeros方法是计算出int类型数据
// 从高位到低位的第一个非0位置前面有多少个0,计算出来的int一定只有低16位才有值
// 假设n的值是32:00000000 00000000 00000000 00100000
// 那么numberOfLeadingZeros方法计算得到的值为26
// 26:00000000 00000000 00000000 00011010
// 1 << (RESIZE_STAMP_BITS - 1) 是 00000000 00000000 10000000 00000000
// 两者做或运算:00000000 00000000 10000000 00011010
// 计算出的结果中包含了当前数组旧长度的信息
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
要点栏
要点1:
扩容前要判断扩容长度是否大于等于最大长度的一半。如果满足直接扩容到最大长度。
这里有一个跳跃式扩容。其实最大长度的一半也是满足2的n次幂的,但是,当扩容长度设置为最大长度的一半时,直接就扩容到最大长度。
如果扩容长度小于最大长度的一半,要确保扩容长度是2的n次幂。
要点2:
ConcurrentHashMap有一个构造方法,入参是map,是将map中的元素赋值到ConcurrentHashMap中。
这个构造方法中会调用tryPresize方法,但是,此时ConcurrentHashMap还未进行初始化。所以在扩容前要判断数组是否已经初始化,以适配这种情况。
要点3:
ConcurrentHashMap有一个putAll方法,入参是map。这个方法中会调用tryPresize方法,扩容长度就是map的长度。但是,map的长度可能小于ConcurrentHashMap的扩容阈值,所有在扩容前要判断扩容长度是否小于扩容阈值,如果是,不需要扩容。
要点4:
扩容前计算出扩容标识戳。这个标识戳是个低16位才有值的int类型,而且低16位的最高位固定是1,其他位记录了数组旧长度信息。
当第一个线程来扩容时,会将扩容标识戳左移16位后,赋值给sizeCtl,并且将sizeCtl加2。这样一来,sizeCtl必定为负数,而且sizeCtl的高16位记录了数组旧长度的信息,低16位记录了有多少线程在扩容。如果低16位是2,代表有一个线程正在扩容,依此类推。
思考栏
tryPresize方法其实可以分成三部分,第一部分是计算出扩容长度,确保不会超过最大长度,以及是2的n次幂;第二部分是为了适配ConcurrentHashMap的构造函数以及putAll方法做的判断;第三部分才是常规情况下的扩容流程。
扩容标识戳可以确保sizeCtl是一个负数。而且高16位包含了数组的旧长度,以此在扩容时可以判断是否有并发情况发生。低16位还包含了扩容线程的个数信息。使用位单位来存储这么多信息,一个int类型就能搞定,占用空间小。
transfer方法
背景栏
MIN_TRANSFER_STRIDE = 16
板书栏
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 根据CPU内核数计算迁移步长,如果步长小于16,赋值为16,否则使用计算出来的步长
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) {
// 进入这里,说明当前线程是第一个扩容的线程
try {
// 以原来数组长度的2倍作为新数组长度,构建新的数组,
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// transferIndex赋值为旧数组长度
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance表示当前线程是否没有获取数据迁移任务
// advance为true表示当前线程没有获取数据迁移任务,为false表示已获取
boolean advance = true;
// finishing表示旧数组中数据是否已经全部迁移
// finishing为true表示已经全部迁移,为false表示还未全部迁移
boolean finishing = false;
// i表示将要迁移的数据索引
// bound表示迁移任务的结束索引
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
// 第一次进入for循环,不可能满足下面的if语句
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;
i = nextIndex - 1;
advance = false;
}
}
// if条件中只有第一个条件可能满足,剩下两个条件永远不会满足
if (i < 0 || i >= n || i + n >= nextn) {
// 进入这里说明没有任务可以领取
int sc;
// 判断旧数组中数据是否都已经迁移结束
if (finishing) {
nextTable = null;
// table赋值为新数组
table = nextTab;
// 重新计算扩容阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 当前线程的迁移任务已经完成,要结束迁移了
// sizeCtl减1,表示参与迁移的线程减少一个
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断当前线程是否是最后一个迁移结束的线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 进入这里当前线程不是最后一个迁移结束的线程,直接reture
return;
// 当前线程是最后一个迁移结束的线程,finishing和advance设置为true
finishing = advance = true;
// i设置成旧数组长度,表示要从头检查一次旧数组中元素是否都迁移完成
i = n;
}
}
else if ((f = tabAt(tab, i)) == null)
// 如果当前索引位置没有数据要迁移
// 将当前节点设置为fwd节点,表示已经迁移完成
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
// 进入这里说明当前节点数据已经迁移完成
advance = true;
else {
synchronized (f) {
// 判断当前节点是否改变过
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 当前节点的hash值大于等于0,才表示是正常节点
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
// for循环是为了获取runBit节点
// 因为一个链表中的数据可能从中间某个节点开始到尾节点,这段节点的迁移位置都相同
// 迁移时,只用将这个节点到尾节点直接迁移到新数组即可
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 如果runBit为0,表示迁移到新数组的位置与旧数组位置相同
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
// 进入这里,表示迁移到新数组的位置是旧数组索引+旧数组长度
hn = lastRun;
ln = null;
}
// 从链表的头节点遍历到runBit节点
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;
// 通过遍历双向链表,获取runBit节点
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;
}
}
// 如果挂载在新数组的元素个数小于等于6,就将双向链表转成单向链表
// 如果挂载在新数组的元素个数大于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;
}
}
}
}
}
}
要点栏
要点1:
迁移步长是一个线程在一次迁移任务中,需要迁移的数组索引范围。根据CPU内核数计算出来,最小为16。
要点2:
第一个做数据迁移的线程需要创建新数组,并且是从最高索引开始迁移数据的。
要点3:
线程在领取迁移任务后,会得到迁移的起始索引和结束索引。并且在迁移完一个索引位置的数据后,旧数组的此索引位置的节点设置为FWD节点,FWD节点的hash值是-1,表示此位置数据已经迁移完成。
要点4:
旧数组数据在新数组中的位置只有两种情况,一种是与旧数组是同一位置,另一种是旧数组索引+旧数组长度。这是因为数据在新数组中的索引位置计算方式是:旧数组索引+数据hash值&旧数组长度。数据hash值&旧数组长度的计算结果只有两种,一个是0,一个是旧数组长度。如果是0,数据在新数组上的索引位置还是旧数组索引;如果是旧数组长度,那么新的索引位置就是旧数组索引+旧数组长度。
要点5:
在迁移数据时,使用了runBit机制来提高迁移效率。因为一个链表中的数据可能从中间某个节点开始到尾节点,这段节点的迁移位置都相同。如此一来,直接将这段节点一次性迁移到新数组,可以提高效率。runBit机制就是得出这段节点的起始节点,做一次性迁移。
要点6:
迁移红黑树时,通过双向链表计算出红黑树节点在新数组中的位置。并且,判断红黑树在迁移到新数组后,节点个数是否小于等于6。如果小于等于6,双向链表转成链表挂载在新数组上;否则双向链表转成红黑树挂载在新数组上。
思考栏
数据迁移使用迁移步长,如此一来可以让多个线程来进行数据迁移,提高迁移效率。
数据迁移到新数组上,索引位置只有两种情况,一种是和旧数组位置相同,一种是旧数组位置+旧数组长度。这与数组长度规定是2的n次幂有关,简化了数据迁移的过程。
使用runBit机制来提高迁移效率,最坏的情况就是链表的尾部节点与前一个节点迁移位置不相同,这样就需要将链表中的节点一个一个的迁移了。
双向链表可以按照情况转变成链表或者红黑树结构,非常灵活,不需要让红黑树转成链表,提高效率。
helpTransfer方法
板书栏
// tbl:旧数组;f:旧数组上的元素
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// nextTab:新数组;sc:sizeCtl的局部变量
Node<K,V>[] nextTab; int sc;
// 第一个判断:旧数组不为null
// 第二个判断:新数组不为null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 计算扩容标识戳
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
// 第一个判断:新数组是否改变。如果改变,说明扩容结束,或者开始新的扩容
// 第二个判断:旧数组是否改变。如果改变,说明扩容结束
// 第三个判断:如果数组正在扩容,sizeCtl值必定小于0
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if (
// 将sc右移16位,判断是否与扩容标识戳一致。如果不一致,说明扩容长度不一样,退出协助扩容
(sc >>> RESIZE_STAMP_SHIFT) != rs
// BUG
// 正确写法:sc == rs << 16 + MAX_RESIZERS
// 判断协助扩容的线程是否已经达到最大值
|| sc == rs + MAX_RESIZERS
// BUG
// 正确写法:sc == rs << 16 + 1
// 判断扩容是否已经到了最后检查阶段
|| sc == rs + 1
// 判断任务是否已经被领完了
|| transferIndex <= 0)
break;
// 将sizeCtl加一,表示多了一个线程协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 协助扩容
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
要点栏
要点1:
线程在协助扩容前,要判断扩容是否已经结束。如果结束,退出协助扩容;如果没有结束,调用transfer方法协助扩容。
红黑树操作
板书栏
红黑树是一种特殊的平衡二叉树,具备平衡二叉树的基本特点:左子树和右子树的高度差不会超过1,如果超过1,会通过左旋或者右旋来实现自平衡。
红黑树在保证自平衡的前提下,还具备自身的一些特点:
- 根节点必须是黑色
- 如果当前节点是红色,其子节点必须是黑色
- 每个节点必须是黑色或者红色
- 所有叶子节点都是黑色,包括空节点
- 从任意节点到每个叶子节点的路径中,黑色节点的数量是相同的
当对红黑树进行增删操作时,可能会破坏平衡或者一些特性,需要基于左旋、右旋或者变色来保证
ConcurrentHashMap中,红黑树结构每次添加节点是当做红色节点添加的。因为添加节点后,要保证从任意节点到每个叶子节点的路径中,黑色节点的数量是相同的这个特点。添加红色节点对此特点影响较小
要点栏
要点1:
ConcurrentHashMap使用红黑树替代链表结构,可以提高查询效率。而且红黑树自平衡效率比平衡二叉树高,在增加和删除元素时,效率比平衡二叉树好。
要点2:
ConcurrentHashMap在新增和删除元素时,主要关注的是红黑树两点:
- 如果当前节点是红色,其子节点必须是黑色
- 从任意节点到每个叶子节点的路径中,黑色节点的数量是相同的
要点3:
ConcurrentHashMap中,红黑树每次添加节点都是当做红色节点添加,这也是为了保证从任意节点到每个叶子节点的路径中,黑色节点的数量是相同的特性。
TreeBin内部类
板书栏
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;
static final int WRITER = 1;
static final int WAITER = 2;
static final int READER = 4;
}
要点栏
要点1:
TreeBin属性中既包含了红黑树,又包含了双向链表
TreeBin构造方法:
板书栏
TreeBin(TreeNode<K,V> b) {
// 将hash值赋值为-2,表示索引位置是红黑树结构
super(TREEBIN, null, null, null);
// 将双向链表的头节点赋值给first
this.first = b;
// r最后是要作为红黑树的根节点
TreeNode<K,V> r = null;
// 遍历双向链表
for (TreeNode<K,V> x = b, next; x != null; x = next) {
// 将当前节点的下一个节点赋值给next
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
// 进入这里,说明要将双向链表的头节点赋值给r
x.parent = null;
// 此节点设置为黑色
x.red = false;
r = x;
}
else {
// 进入这里,说明不是双向链表的头节点
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 第一次进入for循环,p赋值为双向链表的头节点
for (TreeNode<K,V> p = r;;) {
// dir的正负决定了当前节点是作为左子节点还是右子节点
// 如果dir小于等于0,作为左子结点
// 如果dir大于0,作为右子节点
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
// dir赋值为-1,当前节点要作为左子结点
dir = -1;
else if (ph < h)
// dir赋值为1,当前节点要作为右子节点
dir = 1;
// 下面也是在计算dir的值
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
// 将p赋值为left或者right节点
// 当dir小于等于0时,判断p的左节点是否为null
// 当dir大于0时,判断p的右节点是否为null
// 如果不为null,说明当前节点要存放的位置已经有其他节点了,要再走一次for循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 将当前节点的parent指向父节点
x.parent = xp;
if (dir <= 0)
// dir小于等于0,当前节点作为左子结点
xp.left = x;
else
// dir大于0,当前节点作为右子节点
xp.right = x;
// 红黑树平衡操作,保持结构
r = balanceInsertion(r, x);
break;
}
}
}
}
// 将红黑树根节点赋值给root
this.root = r;
// 检查红黑树结构
assert checkInvariants(root);
}
要点栏
要点1:
构造红黑树时,是生成一个TreeBin对象。TreeBin对象中的root成员变量指向红黑树的根节点,first成员变量指向双向链表的头节点。
要点2:
当前节点的hash值比父节点的小,作为父节点的左子节点,如果比父节点大,作为父节点的右子节点,然后进行一次红黑树自平衡。
思考栏:
ConcurrentHashMap的红黑树节点是TreeBin类型,TreeBin对象也有hash值,并且值是-2,表示此索引位置是红黑树结构。而在操作红黑树的时候,其实操作的是TreeBin对象中的TreeNode对象。
balanceInsertion方法:
板书栏
// root:红黑树的根节点
// x:当前节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 将当前节点设置成红色
x.red = true;
// xp:当前节点的父节点
// xpp:当前节点的爷爷节点
// xppl:当前节点的爷爷节点的左子树
// xppr:当前节点的爷爷节点的右子树
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 判断当前节点的父节点是否为null
if ((xp = x.parent) == null) {
// 进入这里,说明x是根节点
// 将x设置成黑色,然后返回
x.red = false;
return x;
}
// 判断当前节点的父节点是否为黑色
// 并且判断当前节点的爷爷节点是否为null
else if (!xp.red || (xpp = xp.parent) == null)
// 进入这里只有一种情况,当前节点的父节点就是根节点
return root;
if (xp == (xppl = xpp.left)) {
// 进入这里说明当前节点在爷爷节点的左子树
if ((xppr = xpp.right) != null && xppr.red) {
// 这里只需要通过变色来保证红黑树结构
// 进入这里说明爷爷节点的右子树不为null
// 并且爷爷节点的右子节点为红色
// 将爷爷节点的右子节点设置为黑色
xppr.red = false;
// 将父节点设置为黑色
xp.red = false;
// 将爷爷节点设置为红色
xpp.red = true;
// 以上操作可以保证爷爷节点以下的节点为红黑树结构,不用再动
// 将x赋值为爷爷节点,再走一次循环,只用考虑包括爷爷节点在内的以上的节点了
x = xpp;
}
else {
// 这里需要通过旋转和变色来保证红黑树结构
// 判断当前节点是否是父节点的右子节点
if (x == xp.right) {
// 当前节点是父节点的右子节点
// 将父节点赋值给x,并且将父节点左旋
// 这样右子树就变成左子树了
// 而且原来的父节点变成了子节点
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
// 将父节点设置黑色
xp.red = false;
if (xpp != null) {
// 将爷爷节点设置为红色
xpp.red = true;
// 将爷爷节点右旋
root = rotateRight(root, xpp);
}
}
}
}
else {
// 进入这里说明当前节点在爷爷节点的右子树
if (xppl != null && xppl.red) {
// 这里只需要通过变色来保证红黑树结构
// 进入这里说明爷爷节点的左子树不为null
// 并且爷爷节点的左子节点为红色
// 将爷爷节点的左子节点设置为黑色
xppl.red = false;
// 将父节点设置为黑色
xp.red = false;
// 将爷爷节点设置为红色
xpp.red = true;
// 以上操作可以保证爷爷节点以下的节点为红黑树结构,不用再动
// 将x赋值为爷爷节点,再走一次循环,只用考虑包括爷爷节点在内的以上的节点了
x = xpp;
}
else {
// 这里需要通过旋转和变色来保证红黑树结构
// 判断当前节点是否是父节点的左子节点
if (x == xp.left) {
// 当前节点是父节点的左子节点
// 将父节点赋值给x,并且将父节点右旋
// 这样左子树就变成右子树了
// 而且原来的父节点变成了子节点
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
// 将父节点设置黑色
xp.red = false;
if (xpp != null) {
// 将爷爷节点设置为红色
xpp.red = true;
// 将爷爷节点左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
红黑树的左旋和右旋操作的动态演示网站:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
要点栏:
要点1:
首先将新添加的节点设置成红节点,然后进行自平衡操作。
要点2:
如果新添加的节点是根节点,将其设置成黑色,然后返回。如果其父节点是根节点,不需要做自平衡操作,直接返回。
要点3:
如果新添加的节点有父节点以及爷爷节点。当其在爷爷节点的左子树上时,如果爷爷节点的右子节点不为空,并且是红色,那么新添加节点的父节点以及爷爷节点只需要做颜色转换即可,不用进行左旋或者右旋来自平衡。新添加的节点在爷爷节点的右子树上,就要看爷爷节点的左子节点了。如果爷爷节点的另一个子节点为空,或者颜色是黑色,那么就需要通过左旋或者右旋操作来实现自平衡了。
要点4:
红黑树的自平衡是自下而上的,先平衡新添加节点的父节点和爷爷节点,再平衡爷爷节点以上的节点。
红黑树插入元素操作
putTreeVal方法
板书栏:
// h:hash值;k:key;v:value
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 进入for死循环,p赋值为根节点
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 判断p是否为null
if (p == null) {
// 进来说明
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
// 将p的hash值赋值给ph,判断是否大于新元素的hash值
else if ((ph = p.hash) > h)
// p的hash值大于新元素的hash值
// dir赋值为-1,表示新元素要放在左子树上
dir = -1;
else if (ph < h)
// p的hash值小于新元素的hash值
// dir赋值为1,表示新元素要放在右子树上
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
// 进入这里说明,p的key值与新元素key值完全相同
// 直接返回p节点,覆盖操作由putVal方法来完成
return p;
else if ((kc == null &&
// 判断key值类型是否实现了compare,需要基于compare判断
(kc = comparableClassFor(k)) == null) ||
// dir为0,说明基于compare判断也是相同的
(dir = compareComparables(kc, k, pk)) == 0) {
// 进入这里说明基于compare判断也是相同的
// 开始搜索,查看是否有相同的key
if (!searched) {
TreeNode<K,V> q, ch;
// 进来一次后,searched设置为true,以后的for循环再也进不来了
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;
}
// 再次判断hash大小,如果小于等于,返回-1,大于返回1
dir = tieBreakOrder(k, pk);
}
// xp是父节点的临时引用
TreeNode<K,V> xp = p;
// 基于dir判断是插入左子树还是右子树,并且给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();
try {
// 红黑树自平衡
root = balanceInsertion(root, x);
} finally {
// 释放锁
unlockRoot();
}
}
break;
}
}
// 最后检查下红黑树结构
assert checkInvariants(root);
return null;
}
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
要点栏:
要点1:
判断红黑树是否有根节点,如果没有根节点,当前节点设置成根节点。
要点2:
如果红黑树有根节点,遍历红黑树,比较红黑树节点的hash值与新元素hash值大小,找到新元素要挂载的位置。
要点3:
如果红黑树节点的hash值与新元素的hash值相同,判断key对象类型是否实现了比较器。如果实现了,使用比较器比较大小;如果没有实现,使用tieBreakOrder方法比较hash值大小。使用tieBreakOrder方法比较大小,如果是相等,返回的也是负数。
要点4:
如果红黑树中节点与新元素节点的key值相同,那么方法返回红黑树节点对象,等待putVal方法做覆盖操作。
要点5:
新元素节点在添加到红黑树中后,然后进行自平衡操作。这里不用担心在自平衡的时候,有其他线程往红黑树中添加元素。因为putTreeVal方法是在putVal方法中执行的,而执行putTreeVal方法之前,已经对根节点对象加锁。
在自平衡前,尝试获取写锁,是因为可能有线程正在读取红黑树数据。这种情况下是不允许自平衡操作发生的。
要点6:
新元素不仅要添加到红黑树中,还要添加到双向链表中。
红黑树的锁操作
要点栏:
- 如果有读线程在读取红黑树的数据,写线程是要堵塞的
- 如果有写线程在操作红黑树,读线程不会堵塞,会从双向链表中读数据
分析栏:
读线程操作时,写线程堵塞,是为了防止写线程改变红黑树结构,影响到读线程。
写线程操作时,读线程从双向链表中读取数据,可以提高读写并发效率。
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;
// 下面的常量是用于锁状态的运算
// 其实是通过int类型的最低的3位来表示锁状态
// 最低位是1,表示写线程获取到了锁资源
static final int WRITER = 1;
// 第二低位是1,表示有一个写线程正在堵塞等待
static final int WAITER = 2;
// 第三低位是1,表示读线程获取到了锁资源
static final int READER = 4;
// 写锁加锁方法
private final void lockRoot() {
// 通过CAS,尝试将lockState从0改成1
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
// 加锁失败,执行contendedLock方法
contendedLock();
}
// 释放锁方法
private final void unlockRoot() {
// 将lockState改成0
lockState = 0;
}
// 写锁加锁失败后执行的方法
private final void contendedLock() {
// waiting是堵塞标识变量,设置为false,表明当前线程未堵塞等待
boolean waiting = false;
// 进入for死循环
for (int s;;) {
// 将lockState赋值给s,并且判断是否有线程持有锁
if (((s = lockState) & ~WAITER) == 0) {
// 进入这里说明,没有线程持有锁
// 通过CAS,尝试获取写锁
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
// 成功获取写锁
// 判断当前线程是否之前处于等待
if (waiting)
// 当前线程之前处于等待
// 将waiter设置为null
waiter = null;
return;
}
}
// 到这里说明有线程持有锁
// 判断当前线程是否已经处于等待
else if ((s & WAITER) == 0) {
// 当前线程还没有处于等待
// 尝试将lockState的第二低位设置为1,表示当前线程将要堵塞等待
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
// waiting设置为true,表示当前线程将要堵塞
waiting = true;
// waiter设置为当前线程
waiter = Thread.currentThread();
}
}
else if (waiting)
// 挂起当前线程,红黑树的读操作中会唤醒
LockSupport.park(this);
}
}
}
要点栏:
要点1:
TreeBin中有个lockState变量是int类型,通过int类型的最低3位表示锁的状态。最低位表示是否有写锁;倒数第二位表示是否有写线程在堵塞;倒数第三位表示是否有读锁。
要点2:
写线程一开始尝试获取写锁,获取失败后,判断是否有线程获取到写锁或者读锁,如果没有,再尝试获取写锁。如果还是获取失败,判断是否有线程处于获取写锁的堵塞状态,如果没有,当前线程堵塞,lockState的倒数第二位设置成1;如果有,当前线程一直执行for循环,尝试获取写锁或者尝试挂起。
分析栏:
对于要点1,使用int的最低三位表示红黑树的锁状态,节省内存空间。
对于要点2,在一个写线程挂起的情况下,其他写线程不停的执行for循环,不用担心浪费cpu资源,是因为多个写线程进入同一个红黑树进行操作的概率很低。在这个前提下,让其他写线程不停的执行for循环,就不用频繁的挂起和唤醒线程,提高并发效率。
ConcurrentHashMap之get方法
public V get(Object key) {
// tab:数组;e:索引位置对应的元素;n:数组长度;eh:数组中索引位置对应元素的hash值
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 根据key计算出hash值
int h = spread(key.hashCode());
// 首先判断数组是否为null,以及数组是否为空
// 然后判断key的hash值对应的数组索引位置是否有数据
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 数组中索引位置对应的元素hash值是否等于key的hash值
if ((eh = e.hash) == h) {
// 在相等的情况下,判断key值与元素的key值是否相等
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
// 相等的情况下,返回value值
return e.val;
}
// 到这里说明hash值不相等
// 判断数组的索引位置的hash值是否小于0,小于0说明有特殊情况
// 1:索引位置被占用;2:数据正在迁移;3:此处是红黑树结构
else if (eh < 0)
// 特殊情况下,调用find方法获取value值
return (p = e.find(h, key)) != null ? p.val : null;
// 到这里说明此处是链表结构
// 遍历链表拿到value值
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// 到这里说明没有key对应的value值,返回null
return null;
}
find方法
ForwardingNode的find方法:
// 数组扩容,数据迁移的情况
// h:hash值;k:key
Node<K,V> find(int h, Object k) {
// tab赋值为新数组
// 进入for死循环
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
// 判断新数组是否为空,并且hash值的索引所在位置是否为空
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
// 进入for死循环
for (;;) {
int eh; K ek;
// 判断索引位置的hash值是否等于key的hash值
// 以及他们的key值是否相等
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
// 相等则返回Node
return e;
// 索引位置的hash值小于0,特殊情况
if (eh < 0) {
if (e instanceof ForwardingNode) {
// 节点是ForwardingNode类型,说明又发生扩容
// tab赋值新的数组,继续for循环
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
// 红黑树结构
return e.find(h, k);
}
// 链表结构,遍历链表
if ((e = e.next) == null)
return null;
}
}
}
ReservationNode的find方法:
// 这个属于节点被占用的情况
// 这种情况是数据还没有放过来,就当做是没有数据来处理
Node<K,V> find(int h, Object k) {
return null;
}
TreeBin的find方法:
final Node<K,V> find(int h, Object k) {
// 首先判断key值是否为null
if (k != null) {
// 进入for死循环
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
// 判断红黑树是否已经有线程获得写锁,或者是否有线程在等待获取写锁
if (((s = lockState) & (WAITER|WRITER)) != 0) {
// 进入这里说明有线程获得写锁或者有线程在等待获得写锁
// 通过遍历双向链表来获取key的value值
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
// 准备遍历红黑树来获取key的value值
// 将锁标志加4,表示有线程获取了读锁
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
// 获取value值
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
// 获取value值后,将锁标志减4
// 判断锁标志是否是与READER或者WAITER相等
// 如果相等说明此线程是最后一个读线程
// 再判断是否有线程在等待获得写锁,如果有则唤醒该线程
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
findTreeNode方法:
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
// key的hash值要小于根节点的hash值,从左子树开始找
p = pl;
else if (ph < h)
// key的hash值要大于根节点的hash值,从右子树开始找
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
// 找到了value值并返回
return p;
// 到这里说明当前根节点的hash值等于key的hash值
else if (pl == null)
// 没有左子树,所以去右子树找
p = pr;
else if (pr == null)
// 没有右子树,所以去左子树找
p = pl;
// 使用比较器来决定去左子树还是右子树找
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 经过上面的判断,还是不知道要去左子树还是右子树找
// 那么默认就先从右子树找
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
// 右子树没有找到,去左子树找
p = pl;
} while (p != null);
}
return null;
}
ConcurrentHashMap的常用方法
compute方法
compute方法作用是根据key找到ConcurrentHashMap中对应的value值,然后对其进行操作,再放去。如果ConcurrentHashMap中没有对应的key,那么就按照value值为null来进行操作,所以可能会抛空指针异常
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
// 入参的非空判断
if (key == null || remappingFunction == null)
throw new NullPointerException();
// 获取到key的hash值
int h = spread(key.hashCode());
V val = null;
int delta = 0;
int binCount = 0;
// 进入for死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 初始化数组
tab = initTable();
// 判断索引位置是否存在元素
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 索引位置不存在元素
// 初始化一个占位节点
Node<K,V> r = new ReservationNode<K,V>();
// 加锁
synchronized (r) {
// 通过CAS将占位节点放到索引位置
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// 执行函数得到新的value值
if ((val = remappingFunction.apply(key, null)) != null) {
delta = 1;
// 生成新的node节点
node = new Node<K,V>(h, key, val, null);
}
} finally {
// 通过CAS将新的节点放到索引位置
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) {
// 进入这里还是为了计算出新的value值,并且替换旧值
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)))) {
val = remappingFunction.apply(key, e.val);
if (val != null)
e.val = val;
else {
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) {
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);
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;
}
compute、computeIfPresent和computeIfAbsent方法的区别
computeIfPresent和computeIfAbsent其实就是将compute方法拆开成了两个方法
computeIfPresent:要求key在map中必须存在,需要基于oldValue计算出newValue
computeIfAbsent:要求key在map中必须不存在,这样才能基于函数得到newValue存入Map中
compute存在的BUG
compute的BUG是在计算结果的函数中,如果再次调用compute方法,并且,也是对相同的key进行操作,就会造成死锁
public static void main(String[] args) throws ExecutionException, InterruptedException {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.compute("key", (k1, v1) -> {
return map.compute("key", (k2, v2) -> {
return "会导致死锁";
});
});
}
replace方法
replace方法作用是将ConcurrentHashMap中的value替换成新的value,但是在替换前先加锁,并且判断旧value值是否发生变化。如果发生变化,什么也不做;没有发生变化,才进行替换。
public boolean replace(K key, V oldValue, V newValue) {
if (key == null || oldValue == null || newValue == null)
throw new NullPointerException();
return replaceNode(key, newValue, oldValue) != null;
}
final V replaceNode(Object key, V value, Object cv) {
// 获得hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判断数组是否已经初始化
// 如果已经初始化,将索引位置的节点赋值给f
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
// 索引位置的节点的hash为MOVED,说明数组在扩容
// 协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
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)
// 替换新值
e.val = value;
else if (pred != null)
pred.next = e.next;
else
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;
}
merge方法
merge方法在使用时,会有三种效果:
- 如果key不存在,就将新的key和value放到map中
- 如果key存在,就基于函数计算,得到结果
- 如果函数结果不为null,用新值替换旧值
- 如果函数结果为null,直接删除map中的key
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (key == null || value == null || remappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
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)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 进入此处说明map中没有相应的key
// 通过CAS操作,添加key和value
if (casTabAt(tab, i, null, new Node<K,V>(h, key, value, null))) {
delta = 1;
val = value;
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(e.val, value);
if (val != null)
// 函数结果不为null
// 新值替换旧值
e.val = val;
else {
// 函数结果为null
// 删除map中的key
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) {
delta = 1;
val = value;
pred.next =
new Node<K,V>(h, key, val, null);
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r = t.root;
TreeNode<K,V> p = (r == null) ? null :
r.findTreeNode(h, key, null);
val = (p == null) ? value :
remappingFunction.apply(p.val, 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;
}
addCount方法
addCount方法的作用是记录ConcurrentHashMap的元素个数,并且方法要保证并发安全,也要保证效率
addCount不是使用AtomicLong原子类。因为在高并发的情况下,CAS操作失败的线程会不停的尝试,这对系统资源来说是很大的消耗
addCount使用的是类似于LongAdder方法。此方法中有一个数组,数组中每个位置的元素都是0。CAS操作失败的线程会随机定位到数组中某个位置,然后将该位置的元素值加1。最后将数组所有元素求和,结果加到CAS操作的数据上
CounterCell内部类
CounterCell是addCount方法中的数组存储的元素类型,其代码如下:
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
@sun.misc.Contended注解:
此注解的作用是解决缓存行同步带来的性能问题
CPU将主存中的数据读取到CPU缓存中,然后CPU从缓存中拿数据,每次读取一个缓存行的数据,缓存行大小默认是64字节。如果数据被volatile关键字修饰,既要保证其可见性,那么缓存行的任意一个数据被改动,这个缓存行就失效。CPU需要重新去主存中读取数据,有性能消耗
此注解为了解决这种情况,会让注解修改的变量独占一个缓冲行。如果变量大小小于缓存行大小,会用无效字符填充缓存行的空闲位置
此注解修饰在类定义上,那么此类的属性独占一个缓存行,避免缓存行失效带来性能问题
addCount源码
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if (
// 判断counterCells数组是否已经初始化,如果没有初始化,执行下一个判断
(as = counterCells) != null ||
// counterCells数组没有初始化,说明之前没有竞争的情况
// 直接通过CAS操作将BASECOUNT加1
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 出现竞争情况
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 判断counterCells数组是否已经初始化
// 如果没有初始化,执行fullAddCount方法进行初始化
as == null || (m = as.length - 1) < 0 ||
// 随机从counterCells数组中获取一个元素,判断是否为null
// 如果为null,执行fullAddCount方法进行初始化
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// 走到这里说明counterCells数组已经初始化好了
// 通过CAS操作,对counterCells数组中随机元素加1
// 如果成功,直接返回;如果失败,执行fullAddCount方法,然后返回
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// sumCount方法是将counterCells数组中元素求和后,结果加到BASECOUNT上,得到map中元素个数
s = sumCount();
}
// check>=0说明需要扩容,check<0说明不需要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (
// 判断是否达到扩容阈值
s >= (long)(sc = sizeCtl) &&
// ConcurrentHashMap数组不能为空
(tab = table) != null &&
// 数组长度是否到达最大值
(n = tab.length) < MAXIMUM_CAPACITY) {
// 扩容标识戳
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
// sc小于0表示有其他线程正在扩容
if (sc < 0) {
// 判断是否可以协助扩容
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(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 + 2))
transfer(tab, null);
// 重新统计元素个数
s = sumCount();
}
}
}
fullAddCount方法:
此方法是在counterCells数组还未初始化,或者counterCell对象还未初始化,或者发生并发的情况下执行的
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 判断线程的随机数有没有生成,如果没有生成,将其生成
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit();
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false;
// 进入for死循环
for (;;) {
// as是CounterCell数组;a是CounterCell对象;n是CounterCell数组长度;v是value值
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 进入这里说明CounterCell数组已经初始化好了
if ((a = as[(n - 1) & h]) == null) {
// 进入这里说明线程随机数对应的CounterCell是null
// 判断cellsBusy是否为0;如果不为0,说明有其他线程在初始化数组或者CounterCell对象
if (cellsBusy == 0) {
// 生成CounterCell对象
CounterCell r = new CounterCell(x);
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// created赋值为false,表示CounterCell对象还未放到CounterCell数组中
boolean created = false;
try {
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// 将CounterCell对象放到CounterCell数组中
rs[j] = r;
// created赋值为true
created = true;
}
} finally {
// cellsBusy赋值为0,表示当前线程结束CounterCell对象初始化
cellsBusy = 0;
}
if (created)
// CounterCell对象放到CounterCell数组中,跳出for死循环
break;
continue;
}
}
collide = false;
}
else if (!wasUncontended)
// 进入这里说明wasUncontended原来是false,现在改成true
wasUncontended = true;
// 到这里说明线程随机数对应的CounterCell不为null
// 通过CAS将CounterCell对象值加x
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// 修改成功,跳出for死循环
break;
// 判断counterCell数组是否还是原来的数组,如果不是,说明发生扩容
// 判断数组长度是否大于CPU内核数,不想让counterCell数组长度大于CPU内核数
else if (counterCells != as || n >= NCPU)
// 当前线程循环结束,不进行扩容
collide = false;
else if (!collide)
// collide设置为true,准备在下次循环中扩容
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {
// 对CounterCell数组进行扩容操作,容量扩大2倍
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;
}
// 更新线程的随机数,争取下次循环成功
h = ThreadLocalRandom.advanceProbe(h);
}
// 走到这里说明CounterCell数组还未初始化
else if (
// 判断cellsBusy是否为0;如果不为0,说明有其他线程在初始化数组
cellsBusy == 0 &&
// 判断counterCell数组是否发生变化,避免并发情况
counterCells == as &&
// 将cellsBusy值从0改成1,表示当前线程要初始化数组
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// init为false表示数组还未初始化成功
boolean init = false;
try {
// 再次判断counterCell数组是否发生变化,避免并发情况
if (counterCells == as) {
// 初始化数组,默认长度是2
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
// 数组初始化成功,init赋值为true
init = true;
}
} finally {
// cellsBusy赋值为0,表示当前线程结束数组初始化
cellsBusy = 0;
}
if (init)
// 数组初始化完成,退出死循环
break;
}
// 到这里是直接将x加到baseCount变量上
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
}
}
size方法
size方法是统计ConcurrentHashMap中元素个数的,源码如下:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}