JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
在深入JDK1.8的put和get实现之前要知道一些常量设计和数据结构,这些是构成ConcurrentHashMap实现结构的基础,下面看一下基本属性:
// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幂数
private static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
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;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树容量
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 树根节点的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
//扩容总进度
transient volatile int transferIndex;
//转移的时候用的数组
transient volatile Node<K,V>[] nextTable;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
private transient volatile int sizeCtl;
//该属性保存着整个哈希表中存储的所有的结点的个数总和,有点类似于 HashMap 的 size 属性。
transient volatile long baseCount;
基本属性定义了ConcurrentHashMap的一些边界以及操作时的一些控制,下面看一些内部的一些结构组成,这些是整个ConcurrentHashMap整个数据结构的核心
Node
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,源代码如下
static class Node<K,V> implements Map.Entry<K,V> {
//链表的数据结构
final int hash;
final K key;
//val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
//不允许更新value
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
//用于map中的get()方法,子类重写
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable; //扩容是用于迁移的新数组
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null); //特殊的Node结点,hash为MOVED
this.nextTable = tab;
}Node数据结构很简单,从上可知,就是一个链表,但是只允许对数据进行查找,不允许进行修改
TreeNode
TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树源代码如下
static final class TreeNode<K,V> extends Node<K,V> {
//树形结构的属性定义
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; //标志红黑树的红节点
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
//根据key查找 从根节点开始找出相应的TreeNode,
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)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
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;
}
}
TreeBin
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,
所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制,
部分源码结构如下
static final class TreeBin<K,V> extends Node<K,V> { //指向TreeNode列表和根节点
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; // 增加数据时读锁的状态
/**
* 初始化红黑树
*/
TreeBin(TreeNode<K,V> b) {
super(TREEBIN, null, null, null); //hash值为负数
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x); //红黑树调整结构
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
put操作
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) { //onlyIfAbsent:仅仅缺少的时候
if (key == null || value == null) throw new NullPointerException(); //key ,value不允许为null
int hash = spread(key.hashCode()); //两次hash,减少hash冲突,可以均匀分布
int binCount = 0; //用于记录元素个数
for (Node<K,V>[] tab = table;;) { //对这个table进行遍历
Node<K,V> f; int n, i, fh;
//这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果i位置没有桶,就直接无锁CAS插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//如果在进行扩容转移中(当前节点是forwardingNode),则帮助扩容
tab = helpTransfer(tab, f); //后面会详解
else {
V oldVal = null;
//如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
synchronized (f) {
if (tabAt(tab, i) == f) { //f改变再次循环
if (fh >= 0) { //表示该节点是链表结构(红黑树或者正在转移都为负数)
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//这里涉及到相同的key进行put就会覆盖原先的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;
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 说明向链表或者红黑树中添加或修改一个节点成功
//binCount == 0 说明 put 操作将一个新节点添加成为某个桶的首节点
if (binCount != 0) { //如果链表的长度大于8时就会进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal; //不会调用到下面的addCount
break;
}
}
} //for循环结束
// 只有新增1个元素的时候才会调用这个方法
addCount(1L, binCount);//统计size,CAS更新baseCount,并且检查是否需要扩容
return null;
}这个put的过程很清晰,对当前的table进行无条件自循环直到put成功,可以分成以下六步流程来概述
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
- 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
现在我们来对每一步的细节进行源码分析,在第一步中,符合条件会进行初始化操作,我们来看看initTable()方法
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {//空的table才能进入初始化操作
//sizeCtl<0表示其他线程已经在初始化了(这也是一种扩容)或者扩容了,挂起当前线程
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//CAS操作SIZECTL为-1,表示初始化状态
try {
if ((tab = table) == null || tab.length == 0) { //如果没有初始化
//sc 大于零说明容量已经初始化了,否则使用默认容量(构造函数)
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);//记录下次扩容的大小:0.75n,和以前的扩容阀值相对应
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
在第二步中没有hash冲突就直接调用Unsafe的方法CAS插入该元素,进入第三步如果容器正在扩容,则会调用helpTransfer()方法帮助扩容,现在我们跟进helpTransfer()方法看看
/**
*帮助从旧的table的元素复制到新的table中
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//新的table nextTba已经存在前提下才能帮助扩容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length); //扩容戳
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//条件1判断是否为当前扩容戳,条件2判断是否扩容结束,
//和3判断扩容线程是否已经超过最大并发扩容线程数 ,
//(这其实是一个bug,见下面详解)
//条件4判断当前是否有可以分配的任务
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;
}
/**生成表的扩容戳,目标不同的n有不同的扩容戳,主要为了标识这次是针对多大的n做的扩容,为了控制并发
* static final int resizeStamp(int n) {
* return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
* }
* Integer.numberOfLeadingZeros(n)在指定 int 值的二进制补码表示形式中最高位(最左边)的 1 位之前,返回零位的数量
* 例如 n为16的二进制为:0001 0000 则Integer.numberOfLeadingZeros(n)为27,因为n为2的幂次方,因此不同的n此结果也不同
* 然后和(1 << (RESIZE_STAMP_BITS - 1))做位操作|,这里等于(2^15 | n最高位0的个数),由于数组大小最大值不超过
2^30,也就是意味着不同的n得出的resizeStamp()值必定不同并且高位都为0,所以后续的tansfor等方法中
sc=((rs=resizeStamp())<<RESIZE_STAMP_SHIFT ) 的高16位用于记录本次扩容戳,低16位用于记录扩容
线程数量
* (2^15是第16位为1,其它为0,因此其左移16位后符号位为1,结果肯定是个负数)
*/
这里附上 Bug 链接的地址 BUG_ID: JDK-8214427, Bug 描述里给出的解决方案由于我的疏忽一不小心写错了,上文给出的解决方案是正确的
oracle 已经标识这个bug 被修复, 修复代码可以在 jdk-12 中看到: jdk12-ConcurrentHashMap-fixed
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 位移操作挪到了这里, bug 被修复了
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//和1判断扩容线程是否已经超过最大并发扩容线程数
//条件2判断是否扩容结束,因为addCount方法(见下文)设置初始SIZECTL
//低位为2,也就是说第一个扩容线程数记为2,如果线程数是1,说明结束了
//条件4判断当前是否有可以分配的任务
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
sizeCtl 变量的总结 通过上面的分析, 我们发现 sizeCtl 变量在 resizeStamp 的辅助一下, 一个变量担当了 5 种角色, 设计非常精巧
if table未完成初始化:
=0 //未指定初始容量时的默认值
>0 //指定初始容量(非传入值,是2的幂次修正值)大小的两倍
=-1 //表明table正在初始化
else if nextTable为空:
if 扩容时发生错误(如内存不足、table.length * 2 > Integer.MAX_VALUE等):
=Integer.MAX_VALUE //不必再扩容了!
else:
=table.length * 0.75 //扩容阈值调为table容量大小的0.75倍
else:
=-(1+N) //N的低RESIZE_STAMP_SHIFT位表示参与扩容线程数,后面详细介绍
table
所有数据都存在table中,table的容量会根据实际情况进行扩容,table[i]存放的数据类型有以下3种(前2种hash值都为负数,所以大于0可以判断是普通链表):
- TreeBin 用于包装红黑树结构的结点类型 -
- ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一
- Node 普通结点类型,表示链表头结点
其实helpTransfer()方法的目的就是调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作
既然这里涉及到扩容的操作,我们也一起来看看扩容方法transfer()
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//每核处理的量小于16,则强制赋值16
//NCPU为CPU核心数,每个核心均分复制任务,如果均分小于16个
//那么以16为步长分给处理器:例如0-15号给处理器1,
//16-32号分给处理器2。 处理器3就不用接任务了。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//如果nextTab为空则初始化为原tab的两倍,这里只会时单线程进得来,因为这
//初始化了nextTab,addcount里面判断了nextTab为空则不执行扩容任务
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
//构建一个nextTable对象,其容量为原来容量的两倍
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;
//扩容总进度,transferIndex之后的桶都已经分配出去,
//[0,transferIndex)待分配,[transferIndex,+∞)已经分配
transferIndex = n;
}
int nextn = nextTab.length;
// 连接点指针,用于标志位(fwd的hash值为-1,fwd.nextTable=nextTab)
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//桶扩容结束时的标记节点
// 当advance == true时,//每个线程正在处理的桶是否已完成迁移,完成了就向前推进
boolean advance = true;
boolean finishing = false; // //table内所有桶是否都已迁移到nextTable标志位。
for (int i = 0, bound = 0;;) {//i表示数组下标,bound表示分配的任务的左边界,不懂继续往下看
Node<K,V> f; int fh;
//这个 while 循环的目的就是通过 --i 遍历当前线程所分配到的桶结点
//一个桶一个桶的处理
while (advance) {
int nextIndex, nextBound;
// 条件1为假说明当前线程负责的桶处理完已经处理完了(超过左边界了),需要重新分配任务
if (--i >= bound || finishing)//倒序处理职责内节点,从右到左处理[nextBound,nextIndex)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {//为true表明要迁移的桶都已分配完毕
i = -1; @1
advance = false;
}
// 用CAS计算得到的transferIndex
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//transferIndex减少已分配出去的桶。
//确定当前线程接下来需要迁移的桶的范围[nextBound, nextIndex)
//每个线程执行完之前的任务以后会走这个分支继续获取新的任务,直到所有任务都分配完毕
bound = nextBound;
i = nextIndex - 1; // i设置为第一个需要处理的桶的位置
advance = false;
}
} //while循环结束
//当前线程自己的活已经做完或所有线程的活都已做完。
//条件1 见前面的@1,目前已经没有要分配的任务了 条件2和3目前什么场景还不知道
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 已经完成所有节点复制了
if (finishing) {
nextTable = null; // 先设置为null
table = nextTab; // table 指向nextTable
sizeCtl = (n << 1) - (n >>> 1);//sizeCtl阈值为原来的1.5倍,为nextTable的0.75
return; // 跳出死循环,
}
// CAS 更扩容阈值,在这里面sizectl值减一,扩容线程数减1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//下面等式右边+2为第一个线程扩容时的值。见addCount()
//因为如果他们相等了,说明没有其他线程在帮助他们
//扩容,当前线程是最后一个线程,而当前线程执行完了,所以,扩容结束了。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
// 再次循环会再次走入这个分支,线程数减1,所有扩容线程执行完毕
// 扩容结束,最终SIZECTL低位线程数为1,这是外界判断是否结束扩容的依据之一
// 比如上面的helpTransfor方法的条件 sc == rs + 1
i = n;
}
}
// 遍历的节点为null,则放入到ForwardingNode ,标识已经处理过了(不用处理)
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了
// 这里是控制并发扩容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // 该位置的桶已经处理过了,跳过即可
else {
synchronized (f) {// 到这里,说明这个位置有实际值了,且不是占位符。对这个节点上锁。为什么上锁,防止 putVal 的时候向链表插入数据
// 节点复制工作
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;// low, height 高位桶(1+n),低位桶(n)
// fh >= 0 ,表示为链表节点
if (fh >= 0) {
// 由于数组长度n是2的次幂,由于定位数组的算法为i=h&(n-1),而新的数组长度
//为2n,那么迁移到新数组的索引为i=h&(2n-1),可能保持不变,也有可能为老索引
//加上n,这取决于节点 hash&n,如果值为0,则索引保持不变,如果值为n,则索引+n
//下面的循环就是要找出最后一段索引不变的链表的开始位置
//最后一段链表是可以直接复用的
int runBit = fh & n; //记录最后停留位置的hash&n值
Node<K,V> lastRun = f;//记录最后停留的结点位置
//整个 for 循环为了找到整个桶中最后连续的 fh & n 不变的结点
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
//只要p.hash&n 改变runBit、lastRun就会改变,所以lastRun
//这个结点到链表末尾这一段链表p.hash&n都是相同的
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {//说明最后一段链表新、老数组索引相同
ln = lastRun;
hn = null;
} else { //说明最后一段链表新索引比老数组索引大n
hn = lastRun;
ln = null;
}
//遍历组装成2个链表
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)
//组装新数组低位i位置的链表(这个链表所在的索引和老数组一样)
ln = new Node<K,V>(ph, pk, pv, ln);
else
// 组装新数组高位i+n位置的链表(这个链表等于老数组索引+n)
hn = new Node<K,V>(ph, pk, pv, hn);
}
//最终链表中元素的中复用的那部分顺序不变,没有复用的部分相对于原链表逆序
// 在新数组nextTable的 i 位置处插上链表
setTabAt(nextTab, i, ln);
// 在新数组nextTable的 i + n 位置处插上链表
setTabAt(nextTab, i + n, hn);
// 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
setTabAt(tab, i, fwd);
// advance = true 可以执行--i动作,遍历节点
advance = true;
}
// 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
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;
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,将树转链表
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;
}
}
}
}
}
所有引起数组扩容的情况如下:
- 链表转换为红黑树时(链表节点个数达到8个可能会转换为红黑树)。如果转换时map长度小于64则直接扩容一倍,不转化为红黑树。如果此时map长度大于64,则不会扩容,直接进行链表转红黑树的操作。
- 当数组中元素达到了sizeCtl的数量的时候,则会调用transfer方法来进行扩容
并发扩容总结
- 单线程新建nextTable,扩容为原table容量的两倍。
- 每个线程想增/删元素时,如果访问的桶是ForwardingNode节点,则表明当前正处于扩容状态,协助一起扩容完成后再完成相应的数据更改操作。
- 扩容时将原table的所有桶倒序分配,每个线程每次最小分16个桶进行处理,防止资源竞争导致的效率下降, 每个桶的迁移是单线程的,但桶范围处理分配可以多线程,在没有迁移完成所有桶之前每个线程需要重复获取迁移桶范围,直至所有桶迁移完成。
- 一个旧桶内的数据迁移完成但迁移工作没有全部完成时,查询数据委托给ForwardingNode结点查询nextTable完成(这个后面看find()分析)。
- 迁移过程中sizeCtl用于记录参与扩容线程的数量,全部迁移完成后sizeCtl更新为新table的扩容阈值。
介绍完扩容过程,我们再次回到put流程,在第四步中是向链表或者红黑树里加节点,到第五步,会调用treeifyBin()方法进行链表转红黑树的过程
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//如果整个table的数量小于64,就扩容至原来的一倍,不转红黑树了
//因为这个阈值扩容可以减少hash冲突,不必要去转红黑树
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
//组装TreeNode链
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对象对TreeNode转换成红黑树
(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
size操作
最后我们来看下例子中最后获取size的方式int size = map.size();,现在让我们看下size()方法
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a; //变化的数量
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value; //数量累加
}
}
return sum;
}可能你会有所疑问,ConcurrentHashMap 中的 baseCount 属性不就是记录的所有键值对的总数吗?直接返回它不就行了吗?
之所以没有这么做,是因为我们的 addCount 方法用于 CAS 更新 baseCount,但很有可能在高并发的情况下,更新失败,那么这些节点虽然已经被添加到哈希表中了,但是数量却没有被统计。
还好,addCount 方法在更新 baseCount 失败的时候,会调用 fullAddCount 将这些失败的结点包装成一个 CounterCell 对象,保存在 CounterCell 数组中。那么整张表实际的 size 其实是 baseCount 加上 CounterCell 数组中元素的个数。
addCount()方法:
// 从 putVal 传入的参数是 1, binCount,binCount 默认是0,只有 hash 冲突了才会大于 1.且他的大小是链表的长度(如果不是红黑数结构的话)。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果计数盒子不是空 或者
// 如果修改 baseCount 失败
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true; // 无竞争的
// 如果计数盒子是空(尚未出现并发)
// 如果随机取余一个数组位置为空 或者
// 修改这个槽位的变量失败(出现并发了)
// 执行 fullAddCount 方法。并结束
// 下面if几个条件在as==null 并且cas修改baseCount失败,或者as!=null的时候判断是否为真
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();
}
// 如果需要检查,检查是否需要扩容,在 putVal 方法调用时,一定为真。
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 如果map.size() 大于 sizeCtl(达到扩容阈值需要扩容) 且
// table 不是空;且 table 的长度小于 1 << 30。(可以扩容)
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 根据 length 得到一个标识
int rs = resizeStamp(n);
// 如果正在扩容
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 扩容
transfer(tab, nt);
}
// 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 更新 sizeCtl 为负数后,开始扩容。
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount方法:
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
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; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
总结一下该方法的逻辑: x 参数表示的此次需要对表中元素的个数加几。check 参数表示是否需要进行扩容检查,大于等于0 需要进行检查,而我们的 putVal 方法的 binCount 参数最小也是 0 ,因此,每次添加元素都会进行检查。(除非是覆盖操作)
- 判断计数盒子属性是否是空,如果是空,就尝试修改 baseCount 变量,对该变量进行加 X。
- 如果计数盒子不是空,或者修改 baseCount 变量失败了,则放弃对 baseCount 进行操作。
- 如果计数盒子是 null 或者计数盒子的 length 是 0,或者随机取一个位置取于数组长度是 null,那么就对刚刚的元素进行 CAS 赋值。
- 如果赋值失败,或者满足上面的条件,则调用 fullAddCount 方法重新死循环插入。
- 这里如果操作 baseCount 失败了(或者计数盒子不是 Null),且对计数盒子赋值成功,那么就检查 check 变量,如果该变量小于等于 1. 直接结束。否则,计算一下 count 变量。
- 如果 check 大于等于 0 ,说明需要对是否扩容进行检查。
- 如果 map 的 size 大于 sizeCtl(扩容阈值),且 table 的长度小于 1 << 30,那么就进行扩容。
- 根据 length 得到一个标识符,然后,判断 sizeCtl 状态,如果小于 0 ,说明要么在初始化,要么在扩容。
- 如果正在扩容,那么就校验一下数据是否变化了(具体可以看上面代码的注释)。如果检验数据不通过,break。
- 如果校验数据通过了,那么将 sizeCtl 加一,表示多了一个线程帮助扩容。然后进行扩容。
- 如果没有在扩容,但是需要扩容。那么就将 sizeCtl 更新,赋值为标识符左移 16 位 —— 一个负数。然后加 2。 表示,已经有一个线程开始扩容了。然后进行扩容。然后再次更新 count,看看是否还需要扩容。
tryPresize()
private final void tryPresize(int size) {
//根据传入的size计算出真正的新容量,新容量需要是2的幂次方。
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1); //检查是否直接设置为最大容量
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c; //table未初始化则给一个初始容量,这边sc可能为Integer.MAX_VALUE
//后面相似代码不再讲解
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
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)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
addCount()与tryPresize()处扩容有何区别?
从上面的分析中我们可以看,addCount()是扩容是老老实实按容量x2来扩容的,tryPresize()会传入一个size参数,可能一次性扩容很多倍。后面采用一样的方式调用transfer()来进行真正的扩容处理。
分析ConcurrentHashMap类的get(int key)方法
//功能:根据key在Map中找出其对应的value,如果不存在key,则返回null,
//其中key不允许为null,否则抛异常
//不用担心get的过程中发生resize,get可能遇到两种情况
//1:桶未resize(无论是没达到阈值还是resize已经开始但是还未处理该桶),遍历链表
//2:在桶的链表遍历的过程中resize,上面的resize分析可以看出并未破坏原tab的桶的节点关系,遍历仍可以继续
//3:读的过程过程正在修改,也没关系
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());//两次hash计算出hash值
if ((tab = table) != null && (n = tab.length) > 0 &&//table不能为null,是吧
(e = tabAt(tab, (n - 1) & h)) != null) {//table[i]不能为空,是吧
if ((eh = e.hash) == h) {//检查头结点
// ek!=null是感觉没有啥用,因为ek不会为null..
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)//table[i]为一颗树或者正在迁移
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//链表,遍历寻找即可
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
可以看到get()操作如果查询链表不用加锁,如果有红黑树结构的话e.find()方法内部实现需要获取锁。
get(int key)方法代码实现流程如下:
- 根据key调用spread计算hash值;并根据计算出来的hash值计算出该key在table出现的位置i.
- 检查table是否为空;如果为空,返回null,否则进行3
- 检查table[i]处桶位不为空;如果为空,则返回null,否则进行4
- 先检查table[i]的头结点的key是否满足条件,是则返回头结点的value;否则分别根据树、链表查询。
看下containsKey/containsValue方法
/*
* Tests if the specified object is a key in this table.
*/
public boolean containsKey(Object key) {
return get(key) != null;//直接调用get(int key)方法即可,如果有返回值,则说明是包含key的
}
/*
*功能,检查在所有映射(k,v)中只要出现一次及以上的v==value,返回true
*注意:这个方法可能需要一个完全遍历Map,因此比containsKey要慢的多
*/
public boolean containsValue(Object value) {
if (value == null)
throw new NullPointerException();
Node<K,V>[] t;
if ((t = table) != null) {
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
for (Node<K,V> p; (p = it.advance()) != null; ) {
V v;
if ((v = p.val) == value || (v != null && value.equals(v)))
return true;
}
}
return false;
} remove方法:
/*
* Removes the key (and its corresponding value) from this map.
* This method does nothing if the key is not in the map.
*/
public V remove(Object key) {
return replaceNode(key, null, null);
}
/*
*如果Map中存在(key,value)节点,则用对象cd来代替,
*如果value为空,则删除此节点。
*/
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());//计算hash值
for (Node<K,V>[] tab = table;;) {//死循环,直到找到
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)//如果为空,则立即返回
break;
else if ((fh = f.hash) == MOVED)//如果检测到其它线程正在扩容,则先帮助扩容,然后再来寻找,可见扩容的优先级之高
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) { //开始锁住这个桶,然后进行比对寻找满足(key,value)的节点
if (tabAt(tab, i) == f) { //重新检查,避免由于多线程的原因table[i]已经被修改
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)))) {//满足条件就是找到key出现的节点位置
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)//value不为空,则更新值
e.val = value;
//value为空,则删除此节点
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);//符合条件的节点e为头结点的情况
}
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)//如果删除了节点,则要减1
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
remove 方法的并发删除过程:
首先遍历整张表的桶结点,如果表还未初始化或者无法根据参数的 hash 值定位到桶结点,那么将返回 null。
如果定位到的桶结点类型是 ForwardingNode 结点,调用 helpTransfer 协助扩容。
否则就老老实实的给桶加锁,删除一个节点。
最后会调用 addCount 方法 CAS 更新 baseCount 的值。
clear方法
clear 方法将删除整张哈希表中所有的键值对,删除操作也是一个桶一个桶的进行删除。public void clear() {
long delta = 0L; // negative number of deletions
int i = 0;
Node<K,V>[] tab = table;
while (tab != null && i < tab.length) {
int fh;
Node<K,V> f = tabAt(tab, i);
if (f == null)
++i;
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);
i = 0; // restart
}
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> p = (fh >= 0 ? f :(f instanceof TreeBin) ?((TreeBin<K,V>)f).first : null);
//循环到链表或者红黑树的尾部
while (p != null) {
--delta;
p = p.next;
}
//首先删除链、树的末尾元素,避免产生大量垃圾
//利用CAS无锁置null
setTabAt(tab, i++, null);
}
}
}
}
if (delta != 0L)
addCount(delta, -1);
}
总结与思考
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考
- JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
- JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
- JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
- JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点
- 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
- JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
- 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
参考文献:
https://www.cnblogs.com/softidea/p/10261414.html