ConcurrentHashMap结构
ConcurrentHashMap是一个线程安全的Map,其通过CAS + synchronized来保证线程操作安全。
参数
静态变量参数:
// 最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认容量
private static final int DEFAULT_CAPACITY = 16;
// 扩容因子,用于判断是否需要扩容
private static final float LOAD_FACTOR = 0.75f;
// 链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 当哈希表中的容量大于这个值时,表中的桶在大于8时才能进行树形化。否则桶内元素太多时会扩容,而不是树形化。
static final int MIN_TREEIFY_CAPACITY = 64;
// 默认线程迁移数据范围
private static final int MIN_TRANSFER_STRIDE = 16;
// 当前服务器cpu数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
属性参数:
// 存储数据的桶,长度为2的整次幂
transient volatile Node<K,V>[] table;
// 扩容时的新桶,解决扩容时访问
private transient volatile Node<K,V>[] nextTable;
//
private transient volatile long baseCount;
// 桶初始化或者扩容时的标记
private transient volatile int sizeCtl;
// 数据迁移的索引
private transient volatile int transferIndex;
//
private transient volatile int cellsBusy;
// 用于计算concurrentHashMap的size,默认长度是2
private transient volatile CounterCell[] counterCells;
构造方法
// 默认构造方法,不设置任何参数
public ConcurrentHashMap() {
}
// 指定桶大小,使用默认的扩容因子,并发级别为1
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
// 指定桶大小和扩容因子,默认并发级别为1
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// 指定桶大小,扩容因子,并发级别
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
// 校验参数
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 至少使用与估计线程数量(并发级别)相同的桶大小
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
// 计算存储initialCapacity个元素,不会触发扩容的最小桶大小
// 待扩容时的大小(initialCapacity) = 桶大小 * 扩容因子(loadFactor)
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// 保证桶的长度为2的整次幂,计算实际桶大小
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
tableSizeFor()可以返回大于c的最小的2的整次幂,和HahsMap中的tableSizeFor()方法作用一致。实现步骤为:
- Integer.numberOfLeadingZeros()返回给定int值(32位)的最左一位之前的0位数。例如12=1100,返回32-4=28
- 将-1 = 1111....1111无符号右移。例如右移28位,得到n = 0000....1111 = 15
- 然后返回n+1,即得到大于c的最小的2的整次幂。例如15+1 = 16 而对于c-1,是为了防止c正好等于2的整次幂返回c的2倍这样的情况。
private static final int tableSizeFor(int c) {
//
int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap的辅助方法
hash机制
ConcurrentHashMap的hash过程和HashMap的hash过程是一样的
- 先将hash值无符号右移16位,即将高16位放置低16位上,高
- 右移后再与原hash值或,即hashCode的高16位与低16位或的结果放在低16位,高16位不变
- 实际使用中Map不过特别大,只使用16位即可,因此高16位与低16与的好处就是可以使用hashcode的所有特征,增加随机性,利于数据分散
// 传入的h是key的hashCode值
// 例如:int h = spread(key.hashCode());
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
ConcurrentHashMap在桶中定位的方法和HashMap也一样:
- n为2的整次幂,例如16 = 10000,减一之后为01111,设1的个数为x=4
- (n-1)与hash与,即保持后x位不变,前(32-x)位全为0,而后x位的值表示的就是该元素的下标
tabAt(tab, (n - 1) & h)
例如,一个值经过hash之后为10100101 11000100 00100101,当前长度为16,即n-1=15,所以有如下结果
10100101 11000100 00100101
& 00000000 00000000 00001111 //15
----------------------------------
00000000 00000000 00000101 //高位全部归零,只保留末四位
将数组长度设置为2的整次幂,当减1之后,得到的数就像一个掩码一样,取后面几位。
整体过程如下:
扩容机制
扩容前准备
扩容时机:
- 新增节点之后,所在链表的元素个数达到了阈值TREEIFY_THRESHOLD=8,当整个桶的元素个数小于MIN_TREEIFY_CAPACITY=64时,进行扩容
- 新增节点之后,元素个数大于桶大小 * 扩容因子时,进行扩容
putVal()方法在放入元素后,会调用addCount()对元素计数,并判断是否需要进行扩容,核心逻辑为:
// check为-1表示删除,通过check判断是否要进行扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// s表示当前桶中的元素个数
// sizeCtl表示扩容的阈值
// 如果元素个数大于扩容阈值,table不为null,长度不超过最大值,则扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 重新更新sizeCtl的值
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
// 已有线程扩容
if (sc < 0) {
// 不帮忙扩容
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
// 线程帮忙扩容,线程数+1
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 没有线程扩容,线程数设置1
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}
其中更新sizeCtl的计算逻辑如下:
- n表示桶的大小,先计算得到n的最左一位之前0的位数,例如n=16=1000,得到32-5 = 27 = 0001 1011
- 1 << (RESIZE_STAMP_BITS - 1) = 1 << 15 = 1000 0000 0000 0000 = 32768
- 第一步和第二步的结果或,最终的结果高16位为0,第16位为1,后15位保存第一步得到的结果,即桶的长度。所以resizeStamp(n)的返回值为:高16位置0,第16位为1,低15位存放当前容量n,用于表示是对n的扩容。
- 再将resizeStamp(n)计算的结果左移16位,即最终的结果rs为第32位为1,31~17位表示当前容量n,低16位全为0,而sc是一个负数
- 如果当sizeCtl>=0,表示还没有线程扩容,则sizeCtl = rs + 2;如果当sizeCtl<0,表示已有线程扩容,现该线程帮忙扩容,线程数+1,则sizeCtl = sizeCtl + 1。因此sizeCtl为第32位为1,31~17位表示当前容量n,低16位表示扩容线程数+1
sizeCtl中即包含了线程数信息,也包含了长度信息,其中长度信息可以保证再次扩容必须等待本次扩容完成之后才能进行。
private static final int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
线程在下面四种情况下不帮忙扩容:
- sc的值是sizeCtl,低16位表示扩容线程数,而MAX_RESIZERS表示最大的扩容线程数,扩容线程数不能超过最大值
- sc的值是sizeCtl,低16位表示的线程数比实际线程数大1,因此sizeCtl的其最小值为rs+1
- 新扩容的桶未创建
- transferIndex在扩容时未初始化
其中1和2是判断sizeCtl的大小范围,3和4是确保只有一个线程初始化扩容工作
// 低15位全为1,高位全为0
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
开始扩容
实际的扩容操作在transfer()方法中完成,该方法的整体逻辑为:
- 先计算每一个线程处理的步长
- 如果扩容的新桶未创建,则创建新桶nextTable,该过程确保只有一个线程进行
- 计算每一个线程处理的范围,即将旧桶的元素移动到新桶的处理范围
- 线程移动分配的范围内的数据
- 将临时扩容的nextTable赋值给table,并再次检查
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 1. 先计算每一个线程处理的步长
int n = tab.length, stride;
// 处理步长和CPU的核数有关,最小为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 2. 初始化新桶
// 由之前对sizeCtl的校验可知,可确保只有一个线程创建新桶
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;
}
nextTable = nextTab;
// transferIndex表示已经处理的范围大小
// transferIndex倒叙表示,减到0表示全部处理完,n表示旧桶的大小
transferIndex = n;
}
// 3. 计算每个批次处理的范围
// 新桶的长度
int nextn = nextTab.length;
// 构造ForwardingNode用于多线程共同扩容以及扩容时访问
// fwd的hash值是默认的MOVED=-1
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 继续遍历的确认标记
boolean advance = true;
// 所有桶都完成迁移的结束标识
boolean finishing = false;
// 死循环
// i表示该线程处理到的位置,boud表示该线程处理的边界值
for (int i = 0, bound = 0;;) {
// f表示待处理的节点,fh表示节点的hash值
Node<K,V> f; int fh;
// 分配一个线程的处理范围
while (advance) {
int nextIndex, nextBound;
// 是否迁移结束
if (--i >= bound || finishing)
advance = false;
// 迁移总进度小于等于0,表示全部都已迁移完
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 更新transferIndex的值
// (1)nextIndex表示transferIndex的旧值
// (2)nextBound表示transferIndex的新值
// 则[nextBound, nextIndex)表示该线程处理的范围
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 边界值
bound = nextBound;
// 起点值
i = nextIndex - 1;
advance = false;
}
}
// 5. 将临时扩容的nextTable赋值给table,并再次检查
// 当迁移完时,i=-1
// 当再次检查时,i=n
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果原table复制结束
if (finishing) {
nextTable = null;
// nextTable是一个中间变量
table = nextTab;
// 更新扩容阈值,现在容量的0.75倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 扩容结束,减少线程数
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 如果不是最后一个线程,直接返回
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果是最后一个线程,更新状态值,并再次检查
finishing = advance = true;
i = n;
}
}
// 4. 线程移动分配的范围内的数据
// (1)节点为空,设置旧桶该位置元素为fwd
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// (2)节点已经被设置为fwd,跳过
else if ((fh = f.hash) == MOVED)
advance = true;
else {
// (3)锁住头节点,移动元素
synchronized (f) {
if (tabAt(tab, i) == f) {
// 和HashMap一样
// 假设,之前数组长度为16(15=1111),现在为32(31=11111)
// (1)当其hash值第5位为1,则在新位置比老位置多了oldCap的长度
// (2)当其hash值第5位为0,则在老位置不变换
// 因此要拆分为两个链表
Node<K,V> ln, hn;
if (fh >= 0) {
// n表示新长度,runBit表示第x位是0还是1
int runBit = fh & n;
Node<K,V> lastRun = f;
// 先遍历链表,找到从某一个节点到最后节点runBit一致的节点
// lastRun表示该节点,该节点往后的所有节点的runBit都一样,即会被移动新桶到同一个位置
// 例如:链表中runBit的值为0,1,1,0,0,0
// 则找到的位置就是第4位,即0,1,1,0(lastRun),0,0
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;
}
// 遍历lastRun之前的节点,安装runBit的位分配至两个链表
// 该分配方式为倒插法,且不是移动元素,而是新建元素
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 计算runBit值
if ((ph & n) == 0)
// 新建元素
// 倒插法,除了lastRun及以后部分,其他都会倒序
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);
// 该位置已被处理,标记为fwd
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;
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
}
}
}
ConcurrentHashMap的操作
添加
添加和扩容都是通过synchronized + CAS实现的
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// (1)初始化桶
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// (2)位置为空,CAS存放节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// (3)正在扩容,则帮忙扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// onlyIfAbsent = false
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
// (4)锁住头节点,遍历查找
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
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<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 链表中binCount代表遍历的深度
if (binCount != 0) {
// 链表长度大于树化阈值,判断是否树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
初始化tab时,需要确保只有一个线程操作的操作为:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl=-1表示正在初始化,让出时间片
if ((sc = sizeCtl) < 0)
Thread.yield();
// 将sizeCtl设置为-1
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
// 初始化tab
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;
sc = n - (n >>> 2);
}
} finally {
// sizeCtl表示扩容阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
sizeCtl使用总结:
- 初始化桶时,sizeCtl=-1,表示正在初始化
- 正常情况下,sizeCtl表示扩容的阈值
- 扩容时,sizeCtl第32位为1,17
31位存放桶长度信息,016位存放扩容线程数
查找
查找操作是一个无锁操作,之所以可以无锁操作,主要可以分为以下几个场景:
- 无扩容,该节点无移动:直接查找,和HashMap一致
- 有扩容,该节点无移动:直接查找,和HashMao一致
- 有扩容,该节点正在移动:仍在旧桶中查找,因为移动元素是创建新的节点,而不是直接移动节点,所以直接直接查找,和HashMap直接移动节点不一致
- 有扩容,该节点已移动:则在ForwardingNode中的nextTable中查找
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算hash值
int h = spread(key.hashCode());
// (1)第一个节点为待查的值
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;
}
// (2)hash值小于0,表示在扩容移动或者为红黑树
// 例如在移动,e是fwd,则调用ForwardingNode的find方法
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// (3)遍历链表查询
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
ForwardingNode的find方法:
Node<K,V> find(int h, Object k) {
// nextTable为新扩容的桶
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
// 返回为null的情况
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
// 循环遍历
for (;;) {
int eh; K ek;
// 第一个节点为待查找的值
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
// 如果待查找位置仍然在扩容移动,递归调用
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}