版本JDK 1.8(内含bug)
数据结构:
ConcurrentHashMap的数据结构和HashMap是一样的哈希表,上面数组,数组的每个数据单元下面是链表或者红黑树,只不过在ConcurrentHashMap上用了
大量的CAS操作,尤其是在putVal()的时候,ConcurrentHashMap通过CAS和Synchronized来保证多线程数据安全。Synchronized相信大家肯定都懂了,
什么是CAS呢?就是全程CompareAndSwap,这个是原子型操作,原理类似乐观锁,举个例子比如a=1,然后我再给a赋值为2的时候,
会先进行比较a是否还等于1,如果等于就把2赋值给a,如果不等于就赋值失败。当然CAS无法解决ABA问题。可以自己去详细了解下CAS。
主要变量介绍:
// 这里指的是整个哈希表 也就是Concurrent
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的数组的初始化大小
private static final int DEFAULT_CAPACITY = 16;
// 前面说过ConcurrentHashMap的数据结构是数组+链表/红黑树
// 这里就是数组的最大长度
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 这个参数在1.8版本不用了,是为了兼容以前的版本才保留的,这个参数在“分段锁”中对应分段锁的个数
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 这个参数就是加载因子
private static final float LOAD_FACTOR = 0.75f;
// 这个参数就是当某个链表的长度达到这个数的时候,数据结构就会由链表变为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 这个参数就是当红黑树的元素数量达到这个参数的时候, 数据结构就由红黑树变为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 在进行链表转换成红黑树之前,一定要满足ConcurrentHashMap的容量最少达到这个参数,不然没办法树化
static final int MIN_TREEIFY_CAPACITY = 64;
// ConcurrentHashMap中的扩容是将老数组下面的所有链表或者红黑树中的所有元素迁移到新的数组上去,
// 在遍历数组的时候可以多个线程一块扩容的,举个具体的例子,
// 比如现在数组的长度是64,有4个线程一起去迁移的话,我们可以一个线程平均迁移16个数组单元中的元素,这里面的16就是对应的这个参数的功能
// 这个参数就是用来定义每次迁移的时候每个线程迁移的数量如果这个值是8的话,A 线程迁移64-56,B线程迁移56-48,C线程迁移48-40,
// D线程迁移40-32.然后哪个线程先迁移完自己负责得部分,再去领32-24这一段的,以此类推,直到完成。
private static final int MIN_TRANSFER_STRIDE = 16;
// 这个参数主要是用来做移位操作的,和位运算相关
private static int RESIZE_STAMP_BITS = 16;
// 主要用来做移位,位运算相关的
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 做位运算相关的
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 这个就是ConcurrentHashMap的数据结构中的数组部分。
transient volatile Node<K,V>[] table;
// ConcurrerentHashMap进行扩容的原理:就是创建一个更大的新数组,长度为2的整次幂,然后将老数组中的数据迁移到新数组上去。
// 这就是那个扩容时候的老数组。
private transient volatile Node<K,V>[] nextTable;
//
private transient volatile long baseCount;
// 这是整个ConcurrentHashMap中最难理解的最复杂的变量,也是用到的地方最多的变量。
/*
sizeCtl:默认为0,用来控制table的初始化和扩容操作.它的数值有以下含义
-1 :代表table正在初始化,其他线程应该交出CPU时间片,退出
-N: 表示正有N-1个线程执行扩容操作
>0: 如果table已经初始化,代表table容量,默认为table大小的0.75,如果还未初始化,代表需要初始化的大小
*/
private transient volatile int sizeCtl;
// 这个是前面说的在进行数据从老数组迁移到新数组的时候,用来记录已经被分配的数组的下标,比如大小为64的数组,
// 如果MIN_TRANSFER_STRIDE = 8,A线程先分配8个数组单元,那么transferIndex就变成了56,B线程再分配8个
// transferIndex就变成了48.... 以此类推。这就是transferIndex的意义。
private transient volatile int transferIndex;
// 在counterCells数组进行初始化或者扩容的时候使用的锁,通过CAS的原理。
private transient volatile int cellsBusy;
// 计数器表,主要用来统计Concurrent
private transient volatile CounterCell[] counterCells;
ConcurrentHashMap的主要操作详解
ConcurrentHashMap的主要操作之putVal——putVal(K key, V value, boolean onlyIfAbsent)
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 判断key 和 value非空,因为ConcurrentHashMap
if (key == null || value == null) throw new NullPointerException();
// 算出key对应的hash值
int hash = spread(key.hashCode());
// 这是个标志位,在添加了新的Node之后,判断是否要进行扩容检查,如果bincount < 0则不进行检查 <=1进行检查
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 判断table是否为空
// 如果为空 初始化Table
if (tab == null || (n = tab.length) == 0)
// 初始化Table
tab = initTable();
// 否则根据这个key对应的hash算出应该放在table数组的哪个下标处,然后判断此位置是否为空,如果为空则直接存储。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 将key value封装成对应的Node 存储在table对应的下标上。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果table对应的数组位置上不为空,且存储的Node的hash 和 moved相等,这里的f是ForwardingNode对象,
// 是这样子的,当我们有的线程在进行putVal的时候,可能存在别的线程正在进行扩容
// (就是将老数组中的值,遍历迁移到新的更大的数组中)。迁移过的数组会被填充一个ForwardingNode对象,用来表示Table中的这个
// 下标下的数据都被迁移完成了,避免了其他线程重复去迁移,ForwardingNode继承了Node类 是ConcurrentHashMap的内部类,
// 可以看一下很简单,
// 如果fh == MOVED 说明ConcurrentHashMap正在扩容,有数据正在进行数据迁移。
else if ((fh = f.hash) == MOVED)
// 该线进入帮助数据进行数据迁移
tab = helpTransfer(tab, f);
// 说明table该下标下已经存储了一部分Node,然后这里对key value进行封装成Node,进行同HashMap一样的常规的数据存储
else {
V oldVal = null;
// 对该下标下的第一个Node进行加锁,相当于锁定了Table这个下标下的所有数据,锁的粒度已经很低了
synchronized (f) {
// 判断Table数组i下标处的Node变了没,
if (tabAt(tab, i) == f) {
// fh >=0, 这里相当于再次进行了fh == MOVED(-1)的判断
if (fh >= 0) {
// 是否进行扩容检查的标志位
binCount = 1;
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果某个已经存储过的Node的key 和hash 和新的key和对应的hash相等,则进行直接用新的value覆盖老的value就行了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 遍历到链表的结尾 直接将key value封装成一个Node 添加到链表的后面
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 判断如果f是红黑树结构 ,则按照红黑树的方式添加该节点
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) {
// 判断是否符合树化(将链表转换成红黑树结构)标准
if (binCount >= TREEIFY_THRESHOLD)
// 进行树化操作
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 对计数器进行判断
addCount(1L, binCount);
return null;
}
ConcurrentHashMap的主要操作之:初始化Table —— initTable()
/*
这个方法主要是为了初始化Table数组,并且为了保证只有一个线程初始化,对SIZECTL变量进行CAS设置,
如果一个线程率先抢占了初始化,将SIZECTL设置为-1,然后别的线程初始化的时候,因为SIZECTL<0,则释放资源给别的线程完成table的初始化。
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 如果table是空或者长度为0 则进行循环
while ((tab = table) == null || tab.length == 0) {
// 判断 SIZECTL的大小,如果小于0,则说明有别的线程正在进行初始化table数组
if ((sc = sizeCtl) < 0)
// 释放资源
Thread.yield();
// 将 SIZECTL设置为-1,然后进行初始化Table
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap的主要操作之: 计数—— addCount(long x, int check)
// x表示添加的key value的个数 默认1L,check表示是否进行扩容检查,就是检查是否要进行扩容
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果 counterCells不为null 或者 设置basecount添加x失败
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))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 如果check大于等于0
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 当s大于或等于sizeCtl(为table容量*0.75),即满足扩容标准,
// 当然如果sizeCtl此时是个负数也满足s>=sizeCtl,什么时候sizeCtl会变成负数呢?注意:下面的分析。
// 且table不为空。table的大小小于最大容量,且将sizeCtl赋值给了sc,此时sc的值就是sizeCtl的值。
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 根据table的大小生成一个标志戳 可以详细了解一下resizeStamp(int n)的原理
// 得出的是一个整数rs,高16bit是0 低16bit的第一个bit是1,也就是说,这个整数的第17比特位是1,
int rs = resizeStamp(n);
/* sizeCtl < 0说明已经有线程将sizeCtrl设置为负数,并且正在进行数据迁移了。此时sizeCtrl的高16位就是
table数组容量的标志戳,代表着table的大小是否发生了变化,低16位记录着正在进行数据迁移的线程的数量。
*/
if (sc < 0) {
/* 将sc右移16位,得到的是高16位是table的数组的大小相关的标志戳,rs则是计算出来的table数组大小相关的最新的标志戳
(sc >>> RESIZE_STAMP_SHIFT) != rs这个就是判断table的数组大小前后是否发生了变化,
sc == rs + 1和sc == rs + MAX_RESIZERS 这两个是JDK1.8的bug,能进入这里的代码就是因为sc<0,
rs我们知道永远是这个整数,那rs+1 和 rs + MAX_RESIZERS怎么可能等于一个负数sc呢?明显的逻辑错误,
正确的应该是:sc == rs<<16 + 1,sc == rs<<16 + MAX_RESIZERS,
sc == rs<<16 + 1判断的是否还有线程在进行数据迁移,
sc == rs<<16 + MAX_RESIZERS判断的是否已经达到最大线程数。
(nt = nextTable) == null判断是否新的table是否已经被置空
transferIndex <= 0 判断从老Table(table)数组进行数据迁移到新Table(nextTable)的下标是否小于0,
因为迁移是从大的下标开始倒着迁移的,所以当transferIndex小于等于0代表迁移结束了
这几个判断条件每一个其实都是在判断数据迁移完成了嘛,如果完成了,就break,结束外层循环。
JDK bug link:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427。
最后这个 BUG 被 JDK 收录。详情可以在 bug link 中查看。
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
/*
如果上面的判断不成立,说明table数组扩容还没执行完,则执行这里的语句,即对SIZECTL进行+1,
前面说过这里的+1其实就是SIZECTL的低16位+1,看下面对SIZECTL的分析就知道,当SIZECTL<0的时候,它的低16位
代表的正是在数据扩容的时候数据从老的table数组迁移到新的nextTable数组的时候,这个过程中参与的线程的数量。
如果执行到这里 进行+1 说明本线程将进行数据迁移,执行下面的transfer(tab, nt);
*/
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
/* 只有第一个线程有机会进入这里,这里是先将rs左移16位,上面分析过,rs的低16位的第一位是1,高16位全是0,
这样左移16位之后,rs整体就是一个负数,且低16位全是0,这个时候rs的高16位就像一个标志戳,
这个标志戳和table数组的容量一一对应,如果这个标志戳变了,
说明table的容量也发生了变化。此时rs的低16位就是用来记录此时参与table扩容迁移数据时的线程数,
第一个开始迁移数据的线程是直接在rs的低16位+2,然后通过CAS赋值给SIZECTL,这样SIZECTL的高16位是标志戳的作用,
用来标记是否table表的大小是否再次被更改,低16位用来标记参与数据迁移的线程数量=SIZECTL的绝对值-1,此时SIZECTL<0;
这也是为什么说只有第一个线程会进入这里。因为后续的线程再拿到SIZECTL值时 SIZECTL<0,会被if判断拦截。
并开始执行下面的transfer()方法,开始数据迁移。
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
ConcurrentHashMap的主要操作之: 帮助迁移 —— helpTransfer(Node<K,V>[] tab, Node<K,V> f)
/*
这个方法是帮助迁移,什么时候会发生迁移呢,肯定是扩容的时候,说明已经发生了扩容,且需要迁移数据,并且迁移数据还未结束的时候,
会调用这个方法,因为这个方法里面的很多细节实在一定的基础和场景上发生的,为了方便理解这个方法,我们先去看一下前面的扩容。
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果旧的table不为空,且f是迁移过的标志ForwardingNode的实例,且新的Table也不空
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 根据老table的长度,返回一个标志戳
int rs = resizeStamp(tab.length);
// 当老的table和新的table均没有发生变化 且sizeCtl<0的时候,进行循环
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
将
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
未完待续
参考链接:
链接: Bug中文详解.