jdk1.7/1.8 ConcurrentHashMap全解析
前言
hashmap是一个性能十分优良的容器,但是在多线程情况下会产生线程安全问题
在JDK1.7中由于采用头插法,在扩容时候会产生死循环造成CPU100%的使用率
而在JDK1.8中虽然采用尾插法,但依旧会造成些许问题,如数据丢失等等问题,
总之,在多线程环境下,如何保证数据的安全性是首要问题。
- 采用Hashtable保证线程安全
hashtable相当简易版的map,只采用链表解决hash冲突,默认初始化容
量为11,key-value都不能为null,扩容是原来大小的2倍+1,这里主要是
说明一下其如何保证线程安全性,如下所示:
public synchronized V remove(Object key) {};
public synchronized V put(K key, V value) {};
public synchronized V get(Object key) {};
public synchronized boolean containsKey(Object key) {};
public synchronized void clear() {};
public synchronized int size() {};
所有关键的方法都使用synchronized保证同一时间内,只能有一个线程成
功获取锁,在多个线程并发执行情况下,读,写操作竞争同一把锁,吞吐
量大大降低。
-
使用ConcurrentHashMap
- 采用分段锁,每一个段Segment继承ReentrantLock进行同步,Segment内包含HashEntry数组,用来存储数据,扩容时候不用全局扩容,只在一个Segment内扩容,用volatile修饰value保证读不加锁。
- Unsafe调用,屏蔽底层操作系统细节,以Api的形式直接访问或操作内存,性能好。
JDK1.7中具体实现
//Segment实现ReentrantLock
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//key-value数组,也就是桶
transient volatile HashEntry<K,V>[] table;
transient int threshold; //阈值
final float loadFactor; //负载因子
}
其数据结构就如下图说所示,相当于使用多把锁提升了并发度
构造方法
public ConcurrentHashMap() {
//空参构造,DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR =0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// MAX_SEGMENTS 值为 65536
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// 这里用来求大于concurrencyLevel最小2 ^ n
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// Segment[]的大小为2的 N 次方,segmentShift属性为32减去N,segmentMask属性为2的N次方减去1
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// c 表示每个 Segment 中应该容纳多少个元素
int c = initialCapacity / ssize;// 除后取整
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 在这里创建数组 segments 和 segment[0]
// 注意只初始化segment[0],其他的段延迟初始化
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 通过 Unsafe 调用, 将 s0 写入 segment[0]
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
put方法如下
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
//进行一次hash散列,定位在哪个segment上
int j = (hash >>> segmentShift) & segmentMask;
//Unsafe系统调用,如果没有初始化segment,先初始化
//注意这里采用延时初始化,节省内存消耗
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//确保初始化过了
s = ensureSegment(j);
// 把元素放置到对应的 segment[j] 中
return s.put(key, hash, value, false);
}
注意这里有个ensureSegment方法
- 这里采用tryLock尝试获取锁,多个线程竞争情况下,当前线程若获取不到锁也不闲着,若对应的桶为空或者遍历到末尾根据Key找不到结点,就先初始化。
- 自旋到一定次数还是获取不到锁才进行阻塞
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
// e用来迭代链表
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // 初始赋值为 -1,进入if (retries < 0) {}的标记
while (!tryLock()) {// 自旋
HashEntry<K,V> f; // 定义临时指针变量, 方便后续判断使用
if (retries < 0) {
// 锁竞争失败,也不闲着,看看有没有其他事可做
if (e == null) {
// 当前桶为空
if (node == null)
//初始化
node = new HashEntry<K,V>(hash, key, value, null);
//跳出该分支的标记
retries = 0;
}
// 根据key 找到node,说明node存在
else if (key.equals(e.key))
//跳出该分支的标记
retries = 0;
else
// 遍历链表
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
//自旋到一定次数就直接阻塞
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
//并发情况下,有其他线程改变结构,所以每隔一次循环之后进行检测
//检查桶中的头结点是否发生变化(可能其他线程进行删除或者添加操作)
// retries重置为 -1,可以继续进行tryLock()尝试获取锁
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
总结一下:
- 通过key的hash值,先获取在segment数组中的下标(高位 & 偏移量)
- 调用Unsafe的getObject获取 segment
- 如果段没有初始化,进行初始化
- 尝试获取锁:
- 获取锁成功直接返回null的Node节点
根据hash定位数组下标,遍历链表
如果头节点为空或者遍历到末尾都没有找到key对应的Node,查看Node节点有没有创建
有的话就设置到头,没有创建就创建Node,有判断是否要扩容,不用扩容就放入 - 当前线程获取锁失败
标记为-1:(在遍历链表过程中,如果链表为空就创建一个Node节点,或者找到key对应的Node就退出标记)
当尝试获取锁失败次数太多(cas)就阻塞
由于尝试获取锁过程中可能有其他线程并发操作,每隔一次循环,就检查首结点,如果不同就将标
- 获取锁成功直接返回null的Node节点
扩容操作
看过HashMap的扩容方法后,相信这里并没有什么困难,不过要注意的是这里有多个for循环,它们分别是做什么的呢?
- 这里多个批量迁移判断,据Doug Lea说,正常情况下,扩容时候,每个桶平均有1 / 6的结点数目的位置发生变化,所以不需要一个一个结点的判断在高位还是低位链表,但是在特殊下,链表中结点位置扩容时重新计算刚好是交错着,这样就不容乐观了。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//2倍于原先的桶数组大小
int newCapacity = oldCapacity << 1;
//新的阈值
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//掩码,用来计算索引
int sizeMask = newCapacity - 1;
//从0号下标开始结点迁移操作
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
//重新计算hash
int idx = e.hash & sizeMask;
//原先桶中只有一个结点,直接放入新的桶数组
if (next == null)
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//这里不同于hashmap中结点是一个一个进行迁移
//意思是说,每个结点都判断一下在高位还是在低
//位链表中,这里是一个批量迁移。
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
//普通的迁移操作,还是一个一个结点进行判断
//这时候可能在高位,也可能在低位
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
//重新计算hash
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//最后将新添加的结点加入到新的桶数组中
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
JDK1.8中具体实现
1.8中实现和之前的大为不同,具体如下
- 摒弃了Segment分段锁,采用Synchronized内置锁(说明1.8的Synchronized性能已经大大改进了,其增加了适应性自旋,可重入,偏向,轻量级锁等,这里先不详细说明)
- 使用CAS方式,通过Unsafe调用直接操作内存,比较替换,相当于volatile语义,实际上也是通过lock前缀指令,根据缓存一致性原则,实现了轻量级的线程同步机制
- 和Hashmap1.8中结构一直,采用数组,链表,红黑树数据结构,将查询时间复杂度降到O(logN)
- 并发扩容,多个线程进行协助扩容
- 并发计数,计算结点个数,并发情况下,多个线程参与计数,采用分段锁思想
- 树结点加入了读写锁机制
下面介绍相关变量
//最大桶数组大小 2 ^ 30,仅仅是为了序列化兼容,在1.8中不使用
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认桶数组大小 16,为了兼容性
private static final int DEFAULT_CAPACITY = 16;
//最大数组数限制
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;
//链表转化为红黑树的条件之一
//只有当桶数组长度大于64,并且TREEIFY_THRESHOLD不小于8才转换
static final int MIN_TREEIFY_CAPACITY = 64;
//步长,扩容操作可以是多线程,扩容时候每个线程分配一个扩容任务,
//也就是负责桶数组上一部分结点迁移操作,默认为16,也就是说,
//一个线程负责16个桶的结点的操作,根据CPU数目计算所得,避免线程数
//过多造成线程上下文切换频繁
private static final int MIN_TRANSFER_STRIDE = 16;
//用于生成扩容戳,用来作为扩容次数的标记,不同的扩容
//次数,扩容戳不同,比如默认桶数组大小为16,第一次扩容
//后为32,扩容戳为
/* 具体计算方法:
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n)
| (1 << (RESIZE_STAMP_BITS - 1));
}
numberOfLeadingZeros这个方法根据传进来的数求出其二进制中第一
不为0之前0的个数,听起来有些拗口
比如32 0000 0000 0000 0000 0000 0000 0010 0000 从第一个bit为1往前有28个0
比如64 0000 0000 0000 0000 0000 0000 0100 0000 从第一个bit为1往前有27个0
假若开始容量是16 ,32是第一次扩容,64显而易见是第二次扩容,区别就是0的个数
在和 1 << (RESIZE_STAMP_BITS - 1) 按位或,保证第16为1
*/
private static int RESIZE_STAMP_BITS = 16;
//扩容时候最大的线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//偏移量,把生成戳移位后保存在sizeCtl中当做扩容线程计数的基数,反方向即可求出扩容戳
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // 表明在扩容阶段
static final int TREEBIN = -2; // 表明是树结点
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // 计算
//核心CPU数目
static final int NCPU = Runtime.getRuntime().availableProcessors();
//桶数组
transient volatile Node<K,V>[] table;
//扩容时的临时桶数组
private transient volatile Node<K,V>[] nextTable;
//互斥变量,该变量有很多用途
private transient volatile int sizeCtl;
//下一个迁移任务在桶数组中的起始位置+1
private transient volatile int transferIndex;
//用来计数,统计桶数组结点个数
private transient volatile long baseCount;
//初始化cell数组的标志
private transient volatile int cellsBusy;
//cell数组,进行并发分段计数
private transient volatile CounterCell[] counterCells;
这里重点介绍一下sizeCtl变量,以便之后的分析:
- 首次初始化时(还没初始化), 其变量含义为初始容量
- 正在初始化时,其值为 -1
- 初始化结束,其值为阈值
- 扩容时候,其为负值,高16位表示扩容戳,低16位表示协助扩容的线程个数
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
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;
}
// ......省略
get方法
- 根据key计算hash
- 边界值判断,判断桶数组是否初始化,并且使用CAS方式判断索引出有没有结点
- 根据hash和key查找value
- 可能是树结点或者是正在扩容阶段
- 普通的链表结点,循环遍历
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
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;
}
put方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value不能为空
if (key == null || value == null) throw new NullPointerException();
// 高16位和低16位按位异或计算hash
int hash = spread(key.hashCode());
// 记录该桶上结点总数量
int binCount = 0;
//循环遍历桶,自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//桶没有初始化,就进行初始化,延时初始化,一会重点介绍
tab = initTable();
//根据key的hash定位在桶数组中下标,Unsafe调用获取首结点
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
}
//如果是扩容阶段,该线程帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//获取当前首节点的对象锁
synchronized (f) {
if (tabAt(tab, i) == f) {
//头节点hash值大于等于0,是正常的链表首节点
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//遍历链表找到key就退出
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//链表中没有key对应的node创建node并退出
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;
}
}
}
}
//判断是否需要将链表转换为树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//map中结点数目加一
addCount(1L, binCount);
return null;
}
initTable方法用来初始化桶数组,注意这里使用延时初始化的方式,并且使用CAS方式保证只能有一个线程进行初始化,初始化时候进行直接将SIZECTL对应内存中的值变为-1,声明为初始化阶段,初始化完毕,SIZECTL表示阈值大小,十分巧妙
//延时初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//自旋,保证至少有一个线程初始化
while ((tab = table) == null || tab.length == 0) {
//表示其他线程正在初始化或者扩容数组,当前线程释放CPU
//yield()方法释放CPU的执行权到就绪状态
if ((sc = sizeCtl) < 0)
Thread.yield();
//当前线程使用CAS操作将字段相对对象地址偏移量上的值改为-1
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;
//初始化完成后,表示阈值(为原来0.75倍)
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc; //初始化完成之后 sizeCtl成为阈值
}
break;
}
}
return tab;
}
并发计数,这里采用类似LongAdder计数工具类
这里采用Segment分段锁思想,多线程环境下使用hash算法初始化cell计数单元进行分段计数最后求和,和java.util.concurrent.atomic.LongAdder相似,避免了多线程的竞争,相对于AtomicInteger并发情况下只能有一个线程获取锁,其他线程自旋,性能更好
扩容
首先介绍几个重要的概念
-
int rs = resizeStamp(n);
计算扩容戳,代表扩容的次数,查看是否是当前桶数组进行的扩容
计算数组大小的二进制中第一个不为0的数之前0的个数和 1 << 15进行按位与操作,
即16位是1,扩容时候,左移16位就成为负数表示扩容阶段 -
注意sizeCtl = -(1 + nThreads)这里用来计算协助扩容数目,实际上并非这样计算,而是使用扩容戳左移16位生成一个负数加上n, n表示扩容的线程数。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//低并发情况下,cell数组不用初始化,CAS操作成功在原始值上计数
//高并发情况下,cell数组若初始化直接进入if条件代码块,
// cell数组没有初始化使用CAS方式修改baseCount失败(有竞争),进入代码块
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//这里cell数组可能为空,直接进入if代码块
//每个线程根据ThreadLocalRandom.getProbe()计算一个随机数,
每个线程计算出的数值不同,之后根据hash算法计算索引,
线程对应的cell没有初始化也进入if代码块
//或者CAS失败进入if条件
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//进行cell数组初始化等一系列操作
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
//并发计数
s = sumCount();
} //和longAdder思想相同,并发计数,线程进行hash运算定位cell,进行初始化技术,最后合并计算结果
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 当计数大于阈值,桶数组不为空,进行扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 计算扩容戳
int rs = resizeStamp(n);
if (sc < 0) {
//当前互斥变量sizeCtl 高16位不等于 rs 说明不是同一个扩容任务,或者没有扩容任务可进行分配直接退出
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == s + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//CAS方式将协助扩容的线程数目加一
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);
s = sumCount();
}
}
}
//协助扩容,若是扩容阶段,当前线程协助扩容
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) {
//计算扩容戳
int rs = resizeStamp(tab.length);
//循环判断是扩容阶段
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//sizeCtl无符号右移RESIZE_STAMP_SHIFT(偏移量默认16)可以反向求出
//扩容戳,这里就比较是否右移之后的值是否等于扩容戳,意思就是说是不是
//当前的扩容任务,再者判断是否还能分配扩容任务
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break; //不能分配扩容任务
//当前进行任务分配,将SIZECTL加一,也就是协助扩容线程数加一
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//进行协助扩容
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
private final void tryPresize(int size) {
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;
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); // 0.75 * n
}
} finally {
sizeCtl = sc;
}
}
}
//c <= sc表示数组已经初始化过, n >= MAXIMUM_CAPACITY表示桶数组到达最大值
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) { //可以进行扩容
//计算扩容戳,代表扩容的次数,查看是否是当前桶数组进行的扩容
//计算数组大小的二进制中第一个不为0的数之前0的个数和 1 << 15进行按位与操作,
//即16位是1,扩容时候,左移16位就成为负数表示扩容阶段
int rs = resizeStamp(n);
//表示正在扩容阶段
if (sc < 0) {
Node<K,V>[] nt;
// 当前互斥变量无符号右移16(默认情况下是16)反向求出当前的扩容戳
// 若不相同,则表示不是一个扩容任务,或者扩容任务为零,没有任务分配
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
//协助扩容,将sizeCtl加一,即协助线程数加一
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//表示第一次扩容,使用cas操作将rs左移16位,即互斥变量是负数,
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//新桶数组还没有初始化
transfer(tab, null);
}
}
}
//真正的结点迁移操作,其实并不是结点移到新的桶数组上,而是采用复制操作
//将结点复制到新的数组上,这样可以保证get时候获取到数据,不过是一种快照读
//综上说明concurrenthashmap是弱一致性的,并不能保证实时数据
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//求取任务步长,第一个任务从数组下标最大开始往前移动一定数目
//4核cpu情况下桶数组长度不大于512,默认步数为16,8核cpu情况下桶数组长度
//不大于1024,默认步数为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // 当前桶数组还未初始化
try {
@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;
}
//concurrenthashmap中成员变量,表示新桶数组
nextTable = nextTab;
//任务索引,开始时候指向桶数组末尾下标,来一个线程分配一定步长的桶
transferIndex = n;
}
int nextn = nextTab.length;
//当迁移线程完成“一个桶”的全部元素的迁移后, 旧数组中该桶所在的位置会被赋成一个 ForwardingNode,当有线程需要访问时候转发到新桶数组上
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //表示一个迁移任务完成,可以开始下一个任务
boolean finishing = false; // 扩容任务结束将此值设为true
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
//表示当前一次扩容任务还未完成,还在当前任务的范围内
if (--i >= bound || finishing)
advance = false;
//从桶数组下标自大而小即从末尾往前遍历,每一个步长分配一个任务,到达头位置结束
//任务分配完毕,当前线程没有迁移任务,可以退出扩容
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false; //退出循环
}
//任务可分配,使用CAS方式将TRANSFERINDEX减小步长数
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 本次迁移任务开始下标
bound = nextBound;
// 本次迁移任务的末尾下标
i = nextIndex - 1;
advance = false;
}
}
//i < 0 :表示本次transfer任务已经执行完毕了
//i >= n :不是同一次扩容任务,任务作废,由最后一个线程检查
//i + n >= nextn :这个条件我不知道怎样理解
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
//重新指向新桶数组
table = nextTab;
//新的阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//迁移任务完成,该线程尝试退出
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//判断自己是不是最后一个迁移任务,不是就直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//是就要将finishing置为true退出循环
finishing = advance = true;
i = n; //这一部分我没有看懂
}
}
else if ((f = tabAt(tab, i)) == null)
//volitail读为空,直接在原来桶数组的对应索引放入转发节点
advance = casTabAt(tab, i, null, fwd);
//已经扩容过了,需要进行检查
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) {
//位标志 节点hash按位与 旧数组大小,求得节点在原来下标出还是,原来下标处+旧数 //组大小
int runBit = fh & n;
Node<K,V> lastRun = f;
//和jdk1.7concurrenthashmap中一致,扩容不是一个一个节点的迁移,而是批量迁移
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);
}
//CAS将高低位链表加入新的桶数组中
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;
}
}
}
}
}
}
总结
如此看来,ConcurrentHashMap真是不容易分析,而且关于Stream部分还未来得及分析,目前先介绍几个重要的方法。