本篇文章接上篇文章,介绍一些ConcurrentHashMap的细节,虽然不是特别重要,但也是干货满满。
先列一个参数:
//0:表示counterCells无人使用
//因为counterCells是一个数组,线程不安全的
//所以在操作counterCells之前,都要用CAS将cellsBusy改成1,表示有人在使用该数组
private transient volatile int cellsBusy;
方法列表
spread():
spread(int h):
//跟hashmap有点区别,不仅高16参与了计算,并且还再次 & 计算
return (h ^ (h >>> 16)) & HASH_BITS;
前面运算这里不解释,跟hashMap一样,主要看后面的& HASH_BITS
HASH_BITS是0x7fffffff,换算下来是2147483649,int的最大值
作用是消除负数,这里顺便看下负数的二进制
举个例子:
-5的原码是:10000000000000000000000000000101
反码是:11111111111111111111111111111010
补码是:11111111111111111111111111111011
负数的原码:
是对应的正数的二进制,但是符号位是1。
符号位是区分正负数用的,1是负数,0是整数(符号位就是最高位的意思,左边第一位)负数的反码:
是除符号位,其余的取反负数的补码:
是反码+1HASH_BITS
换算下来的二进制是:11111111111111111111111111111111
(注意是31位,虽然Java中的int是32位,但是一般只有负数才会显示32位,正数默认31位)hash & HASH_BITS
,不管hash是正还是负,结果都会转成正数,因为HASH_BITS的符号位是0,&
下来的最高位肯定是0,也就是正数。spread(int h)
的作用其实就是避免hash值是负数,大概是因为ConcurrentHashMap内置了MOVED
、TREEBIN
、RESERVED
这3个hash,是负数,为了避免冲突吧。
addCount的第一部分:
//判断是否有失败的风险
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//判断是否可以投机
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//死循环把x记录到counterCells,uncontended是判断有没有尝试用CAS更新过
fullAddCount(x, uncontended);
return;
}
//到这表示CAS把值记到counterCells肯定成功了,因为上面的是 ||,CAS是最后一个判断条件
//check == 0:表示插入的下标是空的,此时是头节点,此坑位还有不少直接return
//check == 1:表示插入的是链表的第2个,此坑位还有不少,就不考虑扩容,直接return
//check == -1:表示的是删除,直接return
//这里跟hashMap有点不一样,没有直接判断大于多少就扩容
if (check <= 1)
return;
//把counterCells的值累加到baseCount
s = sumCount();
}
先介绍一下counterCells
,每次是CAS去增加baseCount
的,这样并发的话肯定会有线程更新失败,这时候ConcurrentHashMap
并没有重试,而是把失败的值记录下来,就是记录到counterCells
里面。
这里可以分为2步:
- 判断是否有失败的风险
- 判断是否可以
投机
(开玩笑)
判断是否有失败的风险
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
(as = counterCells) != null:
很简单,counterCells初始为null,CAS增加baseCount失败就会把值记录到counterCells里
counterCells != null说明出现并发修改baseCount,有线程失败了
已经有线程失败了,那么再CAS新增很有可能失败,所有这里 != null 就没有尝试CAS,直接走下面的逻辑
为null才走CAS,尽可能的避免竞争
判断是否可以投机
//进到这里,表示很有可能多线程在修改baseCount
//ThreadLocalRandom.getProbe()可以简单的先理解成取当前线程的hash,后面再详细分析
//非常Nice的判断
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//死循环把x记录到counterCells,uncontended是判断有没有尝试用CAS更新过
fullAddCount(x, uncontended);
return;
}
as == null || (m = as.length - 1) < 0:
这个判断很简单,判断counterCells是否有值,但是含义却不简单
因为上面已经判断过 != null了,这里再判断一下,为了减少竞争,继续看下面的判断
(a = as[ThreadLocalRandom.getProbe() & m]) == null:
ThreadLocalRandom.getProbe()最后说,先简单理解成线程的hash
这里判断的是根据当前线程推算出要插入的counterCells的下标是否为null
走到这里,counterCells!= null,再判断插入的下标是否= null
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x):
因为if里面是 || 判断,走到这,说明counterCells != null,要插入的counterCells的下标 =null
实在没办法了,必须得竞争了,此时才CAS,把值更新到counterCells 里面
回忆一下,整个判断,是这样的:
- counterCells == null,CAS更新baseCount
- counterCells != null(或者CAS失败),再判断一遍counterCells != null
- counterCells != null,再判断要插入的下标!= null
- 要插入的下标!= null,才CAS把值更新到counterCells
可以看到,尽量的保证进入到fullAddCount
这个死循环的时候,没有竞争。
实在避免不了,再进死循环之前,再尝试一遍CAS,非常nice,Doug Lea
牛逼。
fullAddCount():
继续fullAddCount
:
由于较为复杂,分成3部分看:
- counterCells != null,找到的下标 == null
- 找到的下标 != null
- counterCells == null
counterCells != null,找到的下标 == n
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
//判断当前线程的PROBE是否初始化过
if ((h = ThreadLocalRandom.getProbe()) == 0) {
//为0就先初始化
ThreadLocalRandom.localInit();
//获取值
h = ThreadLocalRandom.getProbe();
//把标志位置为true,确保为true,也就是确保就是没有CAS过
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
//先判断counterCells是否初始化过
if ((as = counterCells) != null && (n = as.length) > 0) {
//h是线程的随机值,(n - 1) & h就是计算下标
if ((a = as[(n - 1) & h]) == null) {
//cellsBusy为0,表示没线程在初始化或者扩容counterCells
if (cellsBusy == 0) {
//生成要插入的对象
CounterCell r = new CounterCell(x);
//把cellsBusy改成1,表示有线程再用
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try {
CounterCell[] rs; int m, j;
//把cellsBusy改成1,再判断一下
//因为改成1之后,counterCells就锁定了,此时的判断就是准的
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
//赋值
rs[j] = r;
created = true;
}
} finally {
//最终将cellsBusy置为0
cellsBusy = 0;
}
if (created)
break;
continue;
}
}
collide = false;
}
总的来说,还是比较简单的,就是一个普通数组,用cellsBusy来保证线程安全。
不过有一个判断比较巧妙,需要单独说一下:
if ((h = ThreadLocalRandom.getProbe()) == 0) {
//记住这个变量
wasUncontended = true;
这里不仅仅是判断PROBE
是否初始化过,还有一个意义wasUncontended
:
其实这里判断的是进方法之前,有没有CAS过
== 0说明没有初始化,说明进fullAddCount是因为counterCells为null
!= 0说明初始化过了,说明进fullAddCount之前已经走过getProbe()了,也就是很有可能CAS过
wasUncontended == true:
说明没有CASwasUncontended == false:
说明CAS失败了
找到的下标 != null
//走到这,说明计算的下标不为null
else if (!wasUncontended)
//进到里面,说明之前在addCount已经CAS过了,并且失败了
//第一次只把标志位改为true,改个状态出去,给个机会,第二次再来CAS,避免竞争
wasUncontended = true;
//走到这,2种可能
//1:是死循环的第二次了
//2:是之前没有CAS过
//不管哪种情况,必须要CAS了,要么是第一次,要么已经让过一次了,只能竞争了。
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
//成功就退出了
break;
//判断一下counterCells是否已经被改掉了或者说数组长度已经 >= CPU数量了
//counterCells 最大是NCPU
else if (counterCells != as || n >= NCPU)
collide = false;
else if (!collide)
//进到里面,说明需要扩容了
//改个状态出去,给个机会
collide = true;
//走到这,没办法了,准备扩容吧
//尝试CAS标志cellsBusy
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
//双重校验,一个意思
if (counterCells == as) {
//扩容2倍
CounterCell[] rs = new CounterCell[n << 1];
//把旧值复制到新数组
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
//最终将cellsBusy置为0
cellsBusy = 0;
}
//改为false,下次还要扩容又要2次判断
collide = false;
//跳出,继续尝试把值新增到数组
continue;
}
//每次不成功就算下新的hash,换个下标试试
h = ThreadLocalRandom.advanceProbe(h);
}
}
}
同样不是很难,只是有一点确实值得借鉴,concurrentHashMap
真的是极力避免竞争,上面说的给个机会,其实相当于空轮询
,竞争失败之后,基本上都要空轮询一次,第2次才会继续竞争。
counterCells == null
//走到这,说明counterCells为null
//就尝试把CELLSBUSY改完1
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try {
// Initialize table
//双重校验,一个意思
if (counterCells == as) {
//初始化长度为2
CounterCell[] rs = new CounterCell[2];
//h是线程的随机值,h & 1就是计算下标
rs[h & 1] = new CounterCell(x);
counterCells = rs;
//设置初始化状态init为true
init = true;
}
} finally {
//最终将cellsBusy置为0
cellsBusy = 0;
}
if (init)
//初始化好了,且新增好了就跳出
break;
}
//走到这,说明已经有线程在初始化counterCells,就再尝试一下baseCount + 1,不行就死循环
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
这也不难,就是普通 数组,线程安全的初始化。
拆开来,貌似都不难,但是值得借鉴的确实多。
ThreadLocalRandom.getProbe():
UNSAFE.getInt(Thread.currentThread(), PROBE)
很简单,就是获取当前线程的某个变量值看下PROBE
:
private static final sun.misc.Unsafe UNSAFE;
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
(tk.getDeclaredField("threadLocalRandomProbe"));
} catch (Exception e) {
throw new Error(e);
}
}
是获取的threadLocalRandomProbe
的初始地址的偏移量,简单理解成获取值。看下threadLocalRandomProbe:
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
就是一个变量,那可以肯定,第一次ThreadLocalRandom.getProbe()
肯定为0
ThreadLocalRandom.localInit():
private static final int PROBE_INCREMENT = 0x9e3779b9;
private static final AtomicInteger probeGenerator = new AtomicInteger();
static final void localInit() {
//很简单,获取一个值0x9e3779b9,-1640531527
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p;
//赋值
UNSAFE.putInt(t, PROBE, probe);
}
说明一下:threadLocalRandomProbe
是Thread的变量probeGenerator 、PROBE_INCREMENT
是ThreadLocalRandom
的,每调一次localInit
,probeGenerator
的值也在变。
看完整个PROBE
的过程,理解成线程的hash是没啥问题的。
ThreadLocalRandom.advanceProbe():
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
一通计算,PROBE
肯定变了,每次换个值,也就是换个下标。
面试题:concurrentHashMap的key和value为啥不能为null
先来回忆一下hashMap:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
concurrentHashMap:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
可以看到,hashMap
是允许的,concurrentHashMap
不允许。
先说value不允许为null:
concurrentHashMap
定位的是并发场景,如果允许value
为null
,那么就会造成一种情况。
A线程:get(key)
为null
,这是A是不知道key
的value
为null
,还是key
不存在
如果是hashMap
,可以用contains
来判断,而concurrentHashMap
不行。
B线程:在Aget(key)
之后put(key,null)
此时Acontains
就是true
,那么就以为key当时是有值的,不知道是B中途插入的。
而hashMap
定位的是单线程,contains
为true
,就表示为key当时是有值的,不考虑B中途插入的情况。
有人会说,别的value
不为null,也会发生这种情况。
没错,但是get(key)
为null
,contains
为true
,A能知道有人中途插入了。先说key不允许为null:
跟value不允许为null:
同样的原因,containsKey
的时候有二义性
来源:oschina
链接:https://my.oschina.net/u/4378647/blog/4794602