文章目录
提前预知
JDK1.7版本的HashMap结构
数组+链表,头插法!
- HashMap多线程下是不安全的。
JDK1.7版本的ConcurrentHashMap结构
数组+链表, 头插法!
ConcurrentHashMap 能够实现线程安全且高效是因为采用了分段加锁的方式,其实就是把一个大ConcurrentHashMap分成了一段一段的 Segment
,称之为段(SEGMENT)。
ConcurrentHashMap 的内部细分了若干个小的 HashMap,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 Segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
这么一理解 ConcurrentHashMap 与 HashTable 最大的区别就是ConcurrentHashMap 对大 table 中每个位置
加锁,而 HashTable如果要加锁的话就是对整个 table 加锁,当然效率就高了。
图中ConcurrentHashMap 有 16 个 Segments,所以理论上,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上,16这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的,每个 Segment 内部更像是一个 HashMap,内部是支持扩容的。
再说说操作ConcurrentHashMap的几个常用的操作方法?
-
get() 方法
:根据 key 找到对应的 Segment,再遍历 key 拿到具体的 HashEntry。 -
put() 方法
:大致是先判断是否需要扩容,扩容整理后根据 key 找到对应的Segment,再往 Segment 中 put 键值对,这个时候 put 是加锁的,利用自旋锁去尝试获取锁,获取锁后判断 key 是否存在,存在就覆盖不存在就添加一个键值对。总之就是利用再入锁的方式锁住Segment,保证只有一个线程在操作 Segment,这就相当于在 HashMap 中保证了只有一个线程在数组的一个位置中 put,这当然不会形成环形链表了。ConcurrentHashMap 初始化的时候会初始化
第一个槽 segment[0]
,对于其他槽来说,在插入第一个值的时候进行初始化,延时初始化。 -
resize()方法
:该方法不需要考虑并发,因为到这里的时候,是持有该 Segment 的独占锁的。 -
get()方法
:该操作是不加锁的。
JDK1.8版本的HashMap结构
数组+链表+红黑树,尾插法!
- HashMap多线程下是不安全的。
JDK1.8版本的ConcurrentHashMap结构
数组+链表+红黑树,尾插法!
JDK1.8 版本的ConcurrentHashMap相比于JDK1.7版本的ConcurrentHashMap变化还是比较大的,首先取消了Segment,结构看起来和JDK1.8版本的HashMap差不多,只不多它是线程安全的。
线程安全是如何实现的那?
put()
的时候采用了 CAS + synchronized 保证线程安全get()
就还是那样,读不影响线程安全,所以变化不大。
JDK1.7和JDK1.8版本的ConcurrentHashMap区别
-
JDK1.8 取消了 Segment 分段锁的数据结构,取而代之的是 数组+链表+红黑树 的结构。
-
JDK1.7采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock。JDK1.8采用 CAS+Synchronized 保证线程安全。
-
JDK1.7原来是对需要进行插入、修改、删除操作的 Segment(一个大TABLE分割成多个小 Segment) 加锁,现调整为对每个数组TABLE加锁(Node)。
-
从原来的遍历链表 O(n),变成遍历红黑树 O(logN)。
-
JDK1.7链表采用头插法,JDK1.8链表采用尾插法。
1.8版本的ConcurrentHashMap底层数据结构
HashMap底层数据结构
在学习ConcurrentHashMap之前最好先学习HashMap
- JDK1.7版本的HsashMap底层的数据结构是:
数组+链表
- JDK1.8版本的HsashMap底层的数据结构是:
数组+链表+红黑树
为什么这样改那?
- 为了效率,大家都知道
树
这种结构存储速度大于数组
,查找速度大于链表
,所以才会采用树这种数据结构。 - 在JDK1.7中,如果链表过于长,就会出现查找效率比较低的情况,所以在JDK1.8中当链表过长就会将其转化为红黑树,为的就是查找的速度更快。
- JDK1.8中,链表长度大于
8
的时候,就会变成红黑树,小于6
的时候,就会有红黑树转化为链表
ConcurrentHashMap底层数据结构
ConcurrentHashMap底层数据结构其实是和HashMap的数据结构是一样的
- JDK1.7版本的HsashMap底层的数据结构是:
数组+链表
- JDK1.8版本的HsashMap底层的数据结构是:
数组+链表+红黑树
那为什么有了HashMap还要ConcurrentHashMap那?
- 因为HashMap是线程不安全的,ConcurrentHashMap是线程安全的,在多线程高并发的情况下,如果要考虑数据的安全性,就要使用ConcurrentHashMap
1.8版本的ConcurrentHashMap中定义的属性
在分析源码之前我们先来看看ConcurrentHashMap中定义了那些属性
/**可以定义的最大容量*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的初始化容量,必须是2的幂
* 为什么必须是2的幂我们后面会详细介绍
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 可能的最大(非 2 的幂)数组大小
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 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;
/**
* 当表的容量大于这个值也可能树化,表树化的最小容量.
* 否则,如果表中的节点过多,则调整表的大小。
* 该值应至少为 4 * TREEIFY_THRESHOLD 以避免调整大小和树化阈值之间发生冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Minimum number of rebinnings per transfer step. Ranges are
* subdivided to allow multiple resizer threads. This value
* serves as a lower bound to avoid resizers encountering
* excessive memory contention. The value should be at least
* DEFAULT_CAPACITY.
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/** CPU 数量,多线程编发扩容时使用*/
static final int NCPU = Runtime.getRuntime().availableProcessors();
1.8版本的ConcurrentHashMap的构造方法
构造方法
我们先从构造方法下手,来慢慢的分析源码:
无参构造
:这个构造方法什么都没做,就是创建了一个ConcurrentHashMap的实例
public ConcurrentHashMap() {
}
带一个参数的构造方法
:传入的参数是数组的大小
public ConcurrentHashMap(int initialCapacity) {
//如果传入的初始化值小于0,直接抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException();
/*
* 如果这个值大于2的29次方,就赋值为MAXIMUM_CAPACITY
* 如果小于这个值,就要调用tableSizeFor来进行计算
* */
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
// 把这个值赋给sizeCtl
this.sizeCtl = cap;
}
带两个参数的构造方法
:传入的值是数组大小和加载因子
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
// 该方法调用了可以接受三个参数的构造方法
this(initialCapacity, loadFactor, 1);
}
带三个参数的构造方法
:传入的值是数组大小,加载因子,并发级别
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//判断是否都大于0
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 判断是否小于默认的并发级别,默认为16
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
// 计算数组大小的size
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// 对传入放入size进行调整,获得真实的数组的大小
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
// 将这个大小赋值给sizeCtl变量
this.sizeCtl = cap;
}
- 聪明的你会发现,我们创建完ConcurrentHashMap的实例之后,
为什么没有开辟数组那
,这就是牵涉到ConcurrentHashMap的延时创建
了,当我们向里面put值的时候才会开辟数组
,这里只是初始化了要开辟数组的大小
:sizeCtl - 后面会重点介绍这个
put方法
!!!!!
tableSizeFor()方法
在有参构造方法中都调用了这个方法,我们跟进看一下它是做什么的?
- 我们跟进一下
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1))
/*
* 这个方法就是保证不管你传入的是什么值,都会转化为比传入的值大的2的幂的数
* 例如:假如你传入一个12,对应的二进制为 :1100 1100>>>1=0110为十进制6
* tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1))
* tableSizeFor(12 + (6) + 1))=tableSizeFor(17)
*
* int n=17-1; 16
* n|=n>>>1; 0001 0000|0000 1000=0001 1000
* n|=n>>>2; 0001 1000|0000 0110=0001 1110
* n|=n>>>4; 0001 1100|0000 0001=0001 1111
* n|=n>>>8; 0001 1111|0000 0000=0001 1111
* n|=n>>>16; 0001 1111|0000 0000=0001 1111
* (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1
* 0001 1111+0000 0001=0010 0000=32 =>2的倍数
* */
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法就是保证不管你传入的是什么值,都会转化为比传入的值大的2的幂的数,这也就是为什么ConcurrentHashMap的容量总是2的N次幂。
为什么必须是2的幂,其他值不行吗?
- 这个问题的答案我们会在后面分析!!!!
1.8版本的ConcurrentHashMap的put方法
put方法
没什么好说的跟进看看putVal
方法就行
public V put(K key, V value) {
return putVal(key, value, false);
}
putVal方法
代码很长,先大致理一下思路再看源码比较轻松:
- 第一个判断:如果传入的key或者value有一个是null,就直接抛出异常
- 进入for循环
- CASE1:如果第一次put值,Node数组还没有初始化,则进入
- CASE2:如果当前位置的值为null,则进入
- CASE3:判断这个Node数组是不是在扩容,如果是,则进入
- CASE4:前面条件执行完就代表有值且产生hash冲突,则进入
- CASE4.1:向链表里面插
- CASE4.2:向树里面插
final V putVal(K key, V value, boolean onlyIfAbsent) {
//如果传入的key或者value有一个是null,就直接抛出异常
if (key == null || value == null) throw new NullPointerException();
//给传入的key计算hash值,然后在使用spread方法在扰动一下:为的就是尽可能的减少hash冲突
int hash = spread(key.hashCode());
//初始化一个整形变量并赋值0,用来记录链表的长度
int binCount = 0;
//这里其实就是自旋操作,因为没有结束条件
for (Node<K,V>[] tab = table;;) {
/**
* f:当前节点
* n:table数组长度
* i:数组索引
* fh:当前节点的hash值
*/
Node<K,V> f; int n, i, fh;
//如果tab为null并且tab的长度为0,说明还没有初始化Node数组
if (tab == null || (n = tab.length) == 0)
//调用initTable方法进行初始化,就是创建一个长度为sizeCtl大小的Node数组,并返回
tab = initTable();
/*
* (f = tabAt(tab, i = (n - 1) & hash))等价于f=tab[i],可以先这个样理解,后面
* 会说明这两中方式的不同
* */
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通过CAS的方式进行添加,是线程安全的。
// 如果cas失败,说明存在竞争,也就是判断完之后,有其他线程捷足先登了
//这个时候方法会返回false,程序向下执行,找到存在hash冲突的方式再存值
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 如果f=tab[i].hash的hash值为-1,表示这个map正在扩容
else if ((fh = f.hash) == MOVED)
//帮助去扩容,多线程扩容,该方法后面会详细介绍
tab = helpTransfer(tab, f);
//前面条件执行完就代表有值且产生hash冲突
else {
V oldVal = null;
//为了并发安全,先把Node数组中的这个tab[i]元素锁住,也就是链表头结点或者树的根节点
synchronized (f) {
//加锁之后重新检查,防止加锁过程中被修改,tab[i]=f
if (tabAt(tab, i) == f) {
//当前节点的hash值不小于0,表示是链表中的节点
if (fh >= 0) {
//计数加1,当大于8的时候就树化
binCount = 1;
//把f赋值给e,保护头结点,记录链表长度
for (Node<K,V> e = f;; ++binCount) {
//用来接收头结点的key
K ek;
//比较头结点的key和要插入的key的hash值,key是否相等
//如果上面判断成功,则进行值的替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//把头结点赋值给pred
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;
}
}
}
}
//如果binCount不为0,就判断是否达到链表转换为红黑树的阈值
//static final int TREEIFY_THRESHOLD = 8;
/*
* 为什么这个树化的方法不加锁?
* 因为在树化的方法内部加了synchronized关键字,一样可以实现同步
* */
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//统计一共put了多少entry到ConcurrentHashMap里面,看看是否达到扩容的阈值
//此方法里面调用了真正的扩容方法,后面会好好分析!!!!
addCount(1L, binCount);
return null;
}
initTable()方法
如果ConcurrentHashMap是第一次put值,会进行初始化,调用这个函数,我们跟进看一看:
- 使用
sizeCtl
初始化数组- CASE1: (sc = sizeCtl) < 0,礼让线程
- CASE2:sc的值不为-1,说明没有其他线程进行初始化数组,就可以进入初始化这个数组。
private final Node<K,V>[] initTable() {
/**
* tab:待初始化的table数组
* sc:数组的长度
*/
Node<K,V>[] tab; int sc;
//把table赋值给tab,如果tab为null且tab长度为0,说明没有初始化,则进入循环体
while ((tab = table) == null || tab.length == 0) {
//来了!!!这个值sizeCtl在这里
if ((sc = sizeCtl) < 0)
//如果sc小于0,就礼让线程,开始自旋,出不来了,等待着sc大于等于0
/**
* 其实sc=sizrCtl,不同的值对应不同的操作
* -1时:表示有线程正在初始化,其他线程进来,就不需要再次初始化,等待其他线程初始化完成即可
* -N时:表示正在进行扩容操作,也需要礼让线程,一个线程扩容即可
* 正数:表示要创建数组的大小
*/
Thread.yield();
/*
* 这里是一个CAS操作:线程安全的替换操作,其实相当于一个锁,当一个线程把sc这个值改为-1的
* 时候,其它线程如果进来就只能因为CASE1条件进入自旋,等待sc这个值大于等于0
*
* 这个SIZECTL是相对于this对象的在内存中的偏移量,指向的就是sizeCtl字段的值
* 如果拿到的这个sc的值和sizeCtl字段的值相同,就将这个sizeCtl字段的值赋值为-1
*
* 为什么前面sc=sizeCtl,这里会不相等那?
* 这个就是多线程操作同一个ConcurrentHashMap的原因
* */
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 初始化数组
try {
//把table赋值给tab,如果tab为null且tab长度为0,则进入
if ((tab = table) == null || tab.length == 0) {
//如果sc大于0,n=sc
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//创建一个长度为n的Node数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 把nt赋值给tab,tab赋值给table
table = tab = nt;
//把sc置为原来大小的0.75倍,扩容阈值
sc = n - (n >>> 2);
}
} finally {
//将sc赋值给sizeCtl,为正数表示要扩容的阈值
sizeCtl = sc;
}
break;
}
}
//返回 tab
return tab;
}
UnSafe.compareAndSwapInt()方法
在初始化的时候,里面调用了这个方法,这是一个本地方法:
/*
* 这里说一下compareAndSwapInt这个方法的参数
* var1:就是将要修改的值的对象
* var2:对象在内存中偏移量为offset处的值,结合object + offset能找到要修改的值的地址.
* var4:期望的值,就是拿这个值和 object + offset处存放的值进行比较;如果相同则修改,返回true,否则返回false,等下次修改.
* var5:如果上一步对比相等,则将这个值替换 object + offset地址处的值,然后返回true。
* */
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
tabAt()方法:
该方法内部调用了一个本地方法Unsafe.getObjectVolatile()
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);
}
Unsafe.getObjectVolatile()
方法:
/*
* 先说一下方法参数:
* var1:就是将要获取的对象
* var2:对象在内存中偏移量为offset处的值,结合var1 + var2能找到对应变量的地址.
*
* 功能:该方法获取对象中offset偏移地址对应的对象field的值,
*
* 为什么使用这种方式获取值?
* getObjectVolatile,一旦看到volatile关键字,就表示可见性
* 以volatile读的方式来读取table数组中的元素,保证每次拿到的数据都是最新的
* 虽然table数组本身是增加了volatile属性,但是volatile的数组只针对数组的引用具有volatile的语义,
* 而不是它的元素,所以如果有其他线程对这个数组的元素进行写操作,那么当前线程来读的时候不一定能读到最新的值
* */
public native Object getObjectVolatile(Object var1, long var2);
casTabAt()方法:
该方法内部调用了一个本地方法Unsafe.compareAndSwapObject()
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
//
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
Unsafe.compareAndSwapObject()
方法:
/*
* 先说一下方法参数:
* var1 :包含要修改的字段对象;
* var2 :字段在对象内的偏移量;
* var4 : 字段的期望值;
* var5 :如果该字段的值等于字段的期望值,用于更新字段的新值;
* 功能:该方法找到offset偏移地址对应的对象field的值,如果和var4相等,就使用var5替换,不相等就自旋
* */
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
helpTransfer()方法
- 如果有线程正在扩容,就会调用helpTransfer方法帮助扩容,方法中会判断是否能够参与此次扩容,如果可以,就会CAS修改sizeCtl,将低16位的值+1,表示正在扩容的线程数加1。
/**
* 如果 resize 操作正在进行,帮助转移节点 f。
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果 tab 不为 null,传进来的节点是 ForwardingNode,且 ForwardingNode
// 的下一个 tab 不为 null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //即nextTable有值,正在扩容。
int rs = resizeStamp(tab.length);//获得容量的标识,
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 不需要帮助转移,跳出循环
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// CAS 更新帮助转移的线程数(+1)
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab; //如果帮助扩容完成了,返回新的nextTab,
}
return table;//扩容完成,那么返回底层table
}
1.8版本的ConcurrentHashMap的扩容原理
addCount()方法
当向ConcurrentHashMap中put
的元素达到了扩容的阈值,就会调用相应的方法进行扩容
在putVal
方法中调用了addCount(1L, binCount);
方法来计算是否达到扩容阈值,我们来看一下:
这里面用到了并发统计相关的类LongAdder,具体的可以参考这篇博客:
https://blog.csdn.net/weixin_45583303/article/details/119323778
/*
* 当数组中的容量大于扩容阈值时:sizeCtl,将进行扩容操作
* */
private final void addCount(long x, int check) {
/**
* as:cells数组
* b:之前put进数组中总的个数,baseCount
* s:更新后的baseCount
*/
CounterCell[] as; long b, s;
/**
* 条件一:如果cells数组不为空,则进入
* 条件二:CSA方式加一失败(存在线程竞争),则进入
*/
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
/**
* a:cell
* v:cells数组对象位置的当前值
* m:cells数组长度
*/
CounterCell a; long v; int m;
//竞争标志位,true表示没有竞争
boolean uncontended = true;
/**
* 条件一:cells数组为空,则进入
* 条件二:cells数组为空,主要是为了获取m值,则进入
* 条件三:如果cells数组不为空,就获取当前线程对应的hash值,通过计算找到cells数组对应的
* 位置,看看是否为null,为null,则进入
* 条件四:条件三不为null,通过CAS对里面存的值进行加1操作,失败则进入(表示有竞争)
* uncontended =false
*
* ThreadLocalRandom.getProbe() & m
* ThreadLocalRandom.getProbe():计算当前线程对应的hash值
* ThreadLocalRandom.getProbe() & m:类似于HashMap中查找桶位置一样,找到在cells数
* 组中的位置
* 注意:base值为CELLVALUE
*/
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 这个就是具体的对发生竞争时如何统计的代码了
// 如果没有看过LongAdder的源码的同学,可以看了LongAdder的源码,再看这个方法的源码
fullAddCount(x, uncontended);
return;
}
// 如果check==binCount<=1,表示不存在多线程put,不需要进行统计操作,直接返回
if (check <= 1)
return;
// 统计一下一共put了多个node到数组中
s = sumCount();
}
/**
* 前面统计完之后,这里就要判断是不是到达扩容阈值了
*/
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
/**
* 条件一:统计的值大于等于扩容阈值
* 条件二:table数组不为null
* 条件三:当前table数组长度小于最大值
* 以上三个条件同时满足,则进入
*/
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 扩容标记。
int rs = resizeStamp(n);
/**
* CASE1:sc<0,则进入
* CSAE2:sc>=0,则进行CAS
*/
//CASE1:sc<0,则进入
if (sc < 0) {
/**
* RESIZE_STAMP_BITS = 16;
* SIZECT为sizeCtl字段的偏移量
* MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1=(1<<16)-1
* MAX_RESIZERS:最大可以帮助扩容的线程数
*
* 条件一:sc无符号右移16为不等于rs,即sc的高16位不等于标识,则进入
* 条件二:rs+1是等于sc,则进入
* 条件三:rs+MAX_RESIZERS等于sc,则进入
* 条件四: nextTable == null,说明当前扩容还没有初始化nextTable,则进入
* 条件五:transferIndex <= 0,不需要线程加入扩容了
*/
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);
}
//CSAE2:sc>=0,则行CAS
// sc=rs左移16为+2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 数据迁移操作
transfer(tab, null);
s = sumCount();
}
}
}
-
这是对判断是否扩容时,while循环里面的条件分析
-
resizeStamp(int n)
方法:计算扩容标记的方法
/**
* numberOfLeadingZeros(n):返回一个整数对应二进制数,高位补的0的个数
* 例如:
* 1==>0000 0000 0000 0000 0000 0000 0000 0001==>32-1=31
* 2==>0000 0000 0000 0000 0000 0000 0000 0010==>32-2=30
* 3==>0000 0000 0000 0000 0000 0000 0000 0011==>32-2=30
* private static int RESIZE_STAMP_BITS = 16;
* RESIZE_STAMP_BITS - 1=15
* 1<< 15=0000 0000 0000 0000 1000 0000 0000 0000
*
* 假如n=16
* 16==>0000 0000 0000 0000 0000 0000 0001 0000==>32-5=27
*
* 0000 0000 0000 0000 0000 0000 0001 1011 = 27
* |
* 0000 0000 0000 0000 1000 0000 0000 0000
* =
* 0000 0000 0000 0000 1000 0000 0001 1011=32795
*
* 注意:这个扩容标记是一个只占用整型低十六位的数,map的容量越大(前面的0越少,求出来的值越少),
* 或运算的结果也就越小
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
transfer()方法
真正的扩容方法是这个方法,addCount方法只是进行统计操作的,达到扩容阈值才会调用这个方法:
这个方法的代码比较长,我们先理一下思路:
-
第一个开始扩容的线程创造一个容量为原容量两倍的新数组。
-
ConcurrentHashMap使用的是多线程扩容,每一个线程完成一段数组中节点的转移,用
stride
控制每个线程每一次需要处理的数组长度,用transferIndex
记录已经处理过或者有线程正在处理的最小槽索引。 -
自旋完成扩容操作,每一次自旋需要判断这一次处理的是哪一个位置,也就是 i 的位置,比 transferIndex 大的索引位置已经分配给之前的线程,当前线程从 transferIndex 位置开始处理,
从后往前
,处理的索引范围是 transferIndex - stride 到 transferIndex
。 -
处理完了当前位置 i 之后,继续处理 --i,如果当前范围内的所有位置都已经处理完了,根据 transferIndex 从 table 又分出一块 stride 给当前线程处理,这一流程是在 while (advance) {…} 这段代码中完成的。
-
确定了应该处理哪一个位置之后,就可以执行转移操作了。
执行转移操作时主要有以下几种情况:
- 如果当前线程已经完成转移,sizeCtl 减一后直接返回,最后一个线程完成扩容,设置 finishing 为 true 表示扩容结束,线程设置好 table、sizeCtl 变量之后,扩容结束。
- 如果 i 位置节点为 null,将其设为 fwd,提醒其他线程该位已经处理过了。
- 如果 i 位置已经处理过了,继续往后处理其他位置。该判断主要是最后i从n到0检查每一个桶是否转移完毕时用到。
- 处理 i 位置。同样地,处理之前使用 synchronized 上锁。
- 无论桶里是链式结构还是树状结构,都将链表拆分成两个链表,分别放在原位置和新位置上。具体实现上,使用了数组容量为 2 的幂这一点来简化操作(只判断标志位),使用了 lastRun 来提高效率。
/**
* 移动和/或复制桶里的节点到新的 table 里。
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 确定步长,表示一个线程处理的数组长度,用来控制对 CPU 的使用,
// 如果Cpu核数只有1,stride为n,如果不为1,则stride = tab.length/(NCPU*8),最小为 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果指定的 nextTab 为空(第一个线程开始扩容),初始化 nextTable
// 其他线程进来帮忙时,不再创建新的 newTable。
if (nextTab == null) { // initiating
try {
// 创建一个相当于当前 table 两倍容量的数组,作为新的 table
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; //初始化完毕,nextTable就不为0,其他线程就可以帮忙转移了。
transferIndex = n; //0到transferIndex的位置是需要转移的桶所在的范围。
}
int nextn = nextTab.length; //新数组的长度,
// fwd 是标志节点。当一个节点为空或者被转移之后,就设置为 fwd 节点
// 表示这个桶已经处理过了
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 标志指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
boolean advance = true;
// 在完成之前重新扫描一遍数组,确认已经完成。
boolean finishing = false; // to ensure sweep before committing nextTab
// 自旋移动每个节点,从 transferIndex 开始移动 stride 个槽的节点到新的
// table
// i 表示当前处理的节点索引,bound 表示需要处理节点的索引边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//这个while在线程第一次进入,会进行CAS划分任务,如果可分配却CAS失败,
// 就会再进while循环,直到得到任务,或者此时扩容任务已全部完成
//在线程分配完任务后,会进第一个if条件,--i向前遍历,直到i到达bound,
//到达后,如果当前transferIndex还是大于0,说明还有任务可以分配,
//所以会进入到CAS中继续分配任务。
//最后的结果就是
//每个线程处理的区间为(nextBound, nextIndex)
while (advance) {
int nextIndex, nextBound;
// 首先执行 i = i - 1,如果 i 大于 bound,说明还在当前 stride 范围内
// nextIndex、nextBound、transferIndex 等都不需要改变
// bound 是所有线程处理区间的最低点
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1; //-1 是为了进入后面的if判断,说明任务完成。
advance = false;
}
// CAS更新 transferIndex,每一次transferIndex会减少一个stride,
// 当前线程处理的桶区间为(nextBound, nextIndex)
// 如果下一个开始往前遍历的起点是比stride大,说明可以进行一次划分任务,
//如果小于等于stride,就说明不可划分了,当前线程的i初始会是-1.
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; //bound为一次任务结束的边界,当i到达bound时,说明线程的任务完成了。
i = nextIndex - 1;
advance = false;
}
}
//如果线程的i为-1,或者有出现扩容冲突,即可能进入到了协助扩容,
// 但是扩容完成了,并且新的扩容开始了,将会导致i比原来的n要大,并且分配到任务但是在这里退出了。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 已经完成转移,设置 table 为新的 table,更新 sizeCtl 为扩容后的
// 0.75 倍(原容量的 1.5 倍)并返回
if (finishing) { //如果全部协助的线程都已经工作完毕,且sizeCtl和原来的值相等,设置了finnishing,说明扩容完成。
nextTable = null; //nextTable赋值为null,方便下次扩容。
table = nextTab; //底层table赋值为新表。
sizeCtl = (n << 1) - (n >>> 1); //设置阈值。
return;
}
// 当前线程 return 之后可能还有其他线程正在转移
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
//然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
//这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// sc 初值为 (rs << RESIZE_STAMP_SHIFT) + 2)
// 如果还有其他线程正在操作,直接返回,不改变 finishing,
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit,i会从n到0开始检查一遍。
}
}
// 如果 i 位置节点为 null,那么放入刚刚初始化的 ForwardingNode ”空节点“,
// 提醒其他线程该位已经处理过了
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置已经处理过了,继续往下
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 处理当前拿到的节点,此处要上锁
synchronized (f) {
// 确认 i 位置仍然是 f,防止其他线程拿到锁进入修改
if (tabAt(tab, i) == f) {
// ln 保留在原位置,hn 应该移到i + n 位置
Node<K,V> ln, hn;
// 如果当前为链表节点
if (fh >= 0) {
// n 为原 table 长度,且为 2 的幂,任何数与 n 进行 & 操作后
// 只可能是 0 或者 n。
// 根据这个把链表节点分成两类,为 0 说明原来的索引小于 n,
// 则位置保持不变,为 n 说明已经超过了原来的 n,新的位置
// 应该是 n + i(n 的某一位为 1,如果需要移动,该 bit 位也
// 必定为 1,不然将会待在原桶,位置不变)
int runBit = fh & n;
Node<K,V> lastRun = f;
//这个for循环,找到最后一个维持不变的lastRun,
//即lastRun后面的节点都是会分到同一个新表中的桶的。
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
// runBit 一直在变化
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 上面的循环执行完之后,lastRun 及其之后的元素在同一组。
// 且 runBit 就是 last 的标识
// 如果 runBit 等于 0,则 lastRun 及之后的元素都在原位置
// 否则,lastRun 及之后的元素都在新的位置
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 把 f 链表分成两个链表。
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);
// i + n 位置
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 上述循环完成转移之后桶内的顺序并不一定是原来的顺序了
// 原因是lastRun后面维持正常顺序,但是头插法会倒序。
// 在 nextTab 的 i 位置插入一个链表
setTabAt(nextTab, i, ln);
// nextTab 的 i + n 位置插入一个链表
setTabAt(nextTab, i + n, hn);
// table 的 i 位置插入 fwd 节点,表示已经处理过了
setTabAt(tab, i, fwd);
advance = true;
} //到这里处理完链表节点的一个桶了。
// 当前为树节点
else if (f instanceof TreeBin) {
// f 转为根节点
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) {
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) //赋值头节点,并设置p.prev
lo = p;
else //赋值普通节点的next。
loTail.next = p;
loTail = p;
++lc;
}
// 应该放在 n + i 位置
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}//到这里红黑树已经分裂成2个双向链表,下一步要进行判断:
// 扩容后不再需要 tree 结构,转变为链表结构,需要就构建红黑树结构。
// 创建 TreeBin 时,其构造函数会把双向链表结构转化成树结构
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); //在新表i位置上,放入新的节点。
setTabAt(nextTab, i + n, hn); //在新表i+n上。放入新节点
setTabAt(tab, i, fwd); //旧表的相应位置。设置为fwd,说明该节点已经处理完毕。
advance = true; //当前节点处理完毕,提示要进行下一个节点处理。
}
}
}
}
}
}