java集合—— ConcurrentHashmap源码解析
该系列都是基于JDK 8
一、基本常量和结构
/** 节点的hash值,这里有三种特殊的,正常的>0 */
static final int MOVED = -1; // 表示该节点槽位正在扩容中
static final int TREEBIN = -2; // 表示该节点是树节点
static final int RESERVED = -3; // 这个类型本章基本操作没有用到不用管
// 默认容量大小
private static final int DEFAULT_CAPACITY = 16;
//默认扩容因子
private static final float LOAD_FACTOR = 0.75f;
//链表长度阈值 树化条件
static final int TREEIFY_THRESHOLD = 8;
//树中只有6个或一下转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//树化的条件之一 数组长度需要达到的值
static final int MIN_TREEIFY_CAPACITY = 64;
// 默认的容器数组
transient volatile Node<K,V>[] table;
// 辅助扩容时使用的数组
private transient volatile Node<K,V>[] nextTable;
//元素计数器
private transient volatile long baseCount;
//表初始化和大小调整控件 有4种情况
//1.sizeCtl为0,代表数组未初始化, 且数组的初始容量为16
//2.sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
//3.sizeCtl为-1,表示数组正在进行初始化
//4.sizeCtl小于0,并且不是-1,表示数组正在扩容
private transient volatile int sizeCtl;
// 扩容时使用 需要转移槽位的索引
private transient volatile int transferIndex;
// 在计算元素个数时,防并发的锁(CAS ),跟下面那个东东配合
private transient volatile int cellsBusy;
// 计算元素个数时使用(防止并发,并发时每个线程都会把当前操作的槽位节点数放入里面最后累计)
// 配合baseCount 使用
private transient volatile CounterCell[] counterCells;
大部分都是常规的常量,但是要记住sizeCtl和节点的特殊hash值,这两者在下面的操作里面扮演着关键角色,从结构上来看基本和HashMap一致 数组+链表/红黑树,如下:
因ConcurrentHashmap的操作思路与HashMap一致有很多地方雷同,所以建议先看看HashMap源码!
二、构造方法
这里只列举三个常用的构造,不过也基本是全部了(容量计算就不说了)
- 无参构造: 啥也没有,注意此时sizeCtl 默认为0
- 初始化容量大小的构造: sizeCtl 为计算后的容量,注意是扩大1.5倍后计算的
- 完整的带参构造: 同样的 sizeCtl为计算后的容量
这时候就会有个疑问,不管怎么初始化就算了个容量?扩容因子、扩容阈值啥都没有,难道和HashMap一样在第一次添加操作的时候,在初始化数组里面完成的?那就直接去看数组的初始化!
源代码如下:
//无参构造
public ConcurrentHashMap() {}
//初始化容量大小的构造
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) {throw new IllegalArgumentException();}
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
// 把传参扩大了1.5倍后计算容量(变成最近的2的n次方数)
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//sizeCtl=容量
this.sizeCtl = cap;
}
//完整的带参构造
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
//sizeCtl=容量
this.sizeCtl = cap;
}
//计算容量的方法 往上找到最近的2的n次方数 比如:7变成8 10变成16
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;
}
三、initTable(容器初始化)
以CAS + 自旋的方式保证初始化的线程安全:
- sizeCtl<0 就代表有其他线程正在扩容或者初始化,所以让出CPU ,让其他线程先上
- 尝试用CAS 把sizeCtl换成-1,失败就继续自旋
- 成功了就初始化容器与扩容阈值,有意思的是扩容阈值的计算(容量-1/4),这也就意味着在构造方法里面指定的扩容因子是不生效的,始终是0.75
此时sizeCtl为扩容阈值!
源代码如下:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 自旋 只要容器数组为空 就不断循环
while ((tab = table) == null || tab.length == 0) {
//sizeCtl,代表着初始化资源或者扩容资源的锁,必须要获取到该锁才允许进行初始化或者扩容的操作
if ((sc = sizeCtl) < 0)
//放弃当前cpu的使用权,让出时间片,线程计入就绪状态参与竞争
Thread.yield();
//CAS 比较并尝试将sizeCtl替换成-1,如果失败则继续循环
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//进行一次double check 防止在进入分支前,容器发生了变更
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;
//容量-容量/4 == 容量*3/4 == 扩容阈值(扩容因子0.75)
sc = n - (n >>> 2);
}
} finally {
// 此时sizeCtl为扩容阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
四、put操作
整体逻辑和HashMap差不多:1.计算hash 2.是否初始化数组 3.是否直接插入 4. 是否插入链表/红黑树
但是加入了线程安全的操作保障(CAS+自旋+synchronized,数组操作全是内存的偏移量 ):
- 计算hash值后,直接死循环(自旋)
- 判断容器数组是否为空,空则初始化容器数组
- 根据hash计算数组下标【(length-1)&hash】,再结合偏移量从数组中取值
- 值为空,说明槽位还没节点,所以可以直接放入该下标对应的槽位(CAS+自旋放入)
- 值不