转载请注明出处:http://blog.csdn.net/linxdcn/article/details/72871732
上一篇并发包-ConcurrentHashMap(一)主要介绍了ConcurrentHashMap的存储结构以及容器的存取方法的源码。
ConcurrentHashMap主要采用上图所示的结构存储元素,在插入第一个元素时采用CAS算法,其余的插入、更新、删除操作均以第一个元素作为锁操作。
本篇主要介绍以下ConcurrentHashMap的size函数以及扩容方法
1 size函数
在ConcurrentHashMap容器中不再在维护一个全容器容量的变量,故每次调用size函数时,需对每个桶的元素个数进行累加,首先来看看几个重要的属性域
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 没有竞争条件时,添加一个元素会直接更新这个值
private transient volatile long baseCount;
// 有竞争时,则更新对应的CounterCell
private transient volatile CounterCell[] counterCells;
// 在扩容和创建CounterCells时使用
private transient volatile int cellsBusy;
}
在ConcurrentHashMap中有两个变量,一个是baseCount,当添加一个元素时,都会尝试去更新这个值。如果更新baseCount失败,才采用CAS算法把值随机更新到counterCells中去。counterCells是一个数组。调用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;
}
可以看到,size函数就是把baseCount和counterCells的数值全部相加,获得那个时刻的容器大小。下面来看看容器是如何更新这两个变量的,当添加一个元素是,都会调用addCount这个函数。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 如果counterCells未初始化,或则更新baseCount失败,即有其他线程竞争
// 则将大小值保存在counterCells中
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 ||
// 在counterCells随机找个位置存放
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 如果更新counterCells也失败了,则调用fullAddCount
// 若counterCells == null,则先初始化
// 然后轮流更新counterCells或baseCount
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
}
2 扩容函数
ConcurrentHashMap扩容的核心函数是transfer,当有其他线程发现该容器在扩容时,会调用helpTransfer来帮助扩容。扩容的内容是申请一个新的2倍大小的数组,将原来数组的元素复制到新数组中。下面是transfer函数的源码解释:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果nextTab为null,则初始化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) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
// 构造一个连节点指针 用于标志位
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false;
// 循环处理所有hash桶
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 找到需要迁移的hash桶
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果所有的节点都已经完成复制工作
// 就把nextTable赋值给table 清空临时对象nextTable
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 利用CAS方法更新这个扩容阈值,
// 在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果遍历到的节点为空,则放入ForwardingNode指针
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 如果遍历到ForwardingNode节点,说明这个点已经被处理过了 直接跳过
// 这里是控制并发扩容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 节点上锁,开始复制到新数组
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
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);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
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;
}
}
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;
}
}
}
}
}
}
元素复制过程跟HashMap是一样的,因为扩容为2的倍数,根据hash桶的计算方法,元素哈希值不变,所以元素在新的hash桶的下标,要不跟旧的hash桶下标一致,要不增加oldSize。
3 总结
- 为什么采用baseCount和counterCells数组来实现size?在高并发情况下,多个线程对同一个变量的修改,很容易就造成性能的严重下降,为了避免这种问题,就给每个hash桶分配一个计数的变量。
- size函数并不保证返回准确的容器大小,因为很有可能在size函数累加各个hash桶数量过程中,某个hash桶的容量就发生了变化。在多线程环境中,一般也只需统计一下大概的容量大小即可。
- 扩容方法的触发:(1)当添加一个元素,使容器大小超过最大限度 * 装载因子时;(2)当添加一个元素,一个hash桶达到树化限度(默认8),且容器容量未超过最小树化容量(默认64)。
转载请注明出处:http://blog.csdn.net/linxdcn/article/details/72871732