ConcurrentHashMap初解
前言
ConcurrentHashMap是Java集合框架中Map接口的一个实现类,与HashMap原理大致相同,不过HashMap是线程不安全的,ConcurrentHashMap是线程安全的
源码解析
构造方法
//无参构造方法
public ConcurrentHashMap() {
}
//带有初始容量的构造函数
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//计算 (1.5*initialCapacity)+1 在向上取整 为2的n次方
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//初始化 table桶长度 2的n次方
this.sizeCtl = cap;
}
//带有初始容量和负载因子的构造函数
//此负载因子之确定初始容量的计算
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
//带有初始容量和负载因子和估计的并发更新线程数
//估计的并发线程数 concurrencyLevel
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//根据 估计的并发更新线程数 计算桶的大小 initialCapacity最少为估计的并发更新线程数
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);
this.sizeCtl = cap;
}
//带有Map集合的构造函数
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public void putAll(Map<? extends K, ? extends V> m) {
//根据给定的大小进项尝试扩容
tryPresize(m.size());
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putVal(e.getKey(), e.getValue(), false);
}
总结
- 包含五个构造方法,可以分别传入初始容量、负载因子、Map集合等
- ConcurrentHashMap默认的负载因为为0.75
- ConcurrentHashMap数组长度最大为2的30次方
- ConcurrentHashMap数组长度必须为2的n次方
- sizeCtl字段
- sizeCtl=0:默认值,表示table初始化使用默认容量 DEFAULT_CAPACITY
- sizeCtl>0:
- table未初始化,sizeCtl表示table初始化时的容量
- table已经初始化,sizeCtl表示table扩容的阈值,当Map中的entry数量>=sizeCtl,则进行扩容
- sizeCtl=-1:表示有线程正在初始化table数组操作,使用initTable方法,使用CAS更新sizeCtl保证只有一个线程初始化成功
- sizeCtl=-(1+nThreads):表示有nThreads个线程正在进行扩容操作
put方法
//放入键值对 返回旧值
public V put(K key, V value) {
return putVal(key, value, false);
}
//
final V putVal(K key, V value, boolean onlyIfAbsent) {
//k和v都不能为空
if (key == null || value == null) throw new NullPointerException();
//计算出k的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();
//tabAt(tab, i = (n - 1) & hash) 获取 tab[(n - 1) & hash]的最新值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//数组桶对应的值为空 以CAS的方式将tab的i节点设置为一个node
//CAS失败则也会跳出循环重新插入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//更新成功直接跳出
break; // no lock when adding to empty bin
}
//表示数组桶正在扩容 表示当前节点为ForwardingNode节点
else if ((fh = f.hash) == MOVED)
//协助扩容
tab = helpTransfer(tab, f);
else { //出现hash冲突
V oldVal = null;
synchronized (f) { //table[i]加锁
//获取的 tab[i]的值没变
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//遍历节点和链表 查找是否含有相同节点更改旧值 或者在链表尾部插入新节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
//同一个key
//如果两个对象相等(equals)hashcode必须相等 hashcode相等的对象equals不一定相等
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;
}
}
}
// 树节点 TreeBin的hash固定值为-2 红黑树节点数组桶上的节点为TreeBin
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)是否需要转化为红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//链表转化为红黑树
treeifyBin(tab, i);
if (oldVal != null)
//本次操作只更新了旧值 直接返回
return oldVal;
break;
}
}
}
//更新Map的元素总数
addCount(1L, binCount);
return null;
}
总结
- put流程
- 根据key的hashCode计算出一个hash值
- 将hash值与数据桶长度-1进行与操作计算出当前key所在的数据桶索引
- 通过tabAt取出当前节点处的值,如当前节点为空则CAS方式插入新节点,如果失败则再次循环
- 如果节点有值,判断节点的hashcode是不是-1如果是-1表示为ForwardingNode节点,当前数组桶正在扩容,则需要协助扩容
- 否则,出现了hash冲突,使用synchronized锁住当前节点,寻找相同对象进行更新,如果没有找到则在节点后新增节点
- 如果当前节点为TreeBin则表示当前是一颗红黑树,则进行红黑树的插入值操作
- 结束后,如果新增了节点则判断是否需要将链表转换为红黑树(链表长度大于等于8)
- 更新了节点返回旧节点,如果增加了节点,则需要将Map数量加一
- 红黑树节点
- 在数组桶上的节点为TreeBin hashcode = -2
- 红黑树上的数据节点为 TreeNode
- 在红黑树的操作过程中会出现旋转操作,可能会改变被锁定的节点位置,而TreeBin 下才是整颗数,在加锁过程中只需锁定这个节点,就锁定了整棵树
链表转化为红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//MIN_TREEIFY_CAPACITY = 64
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;
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;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
总结
- 当一个链表的长度到达需要转化为红黑树的时候 会先判断当前数组桶的长度是否小于64 如果小于64则先进行扩容
- 大于或等于64则需要进行树化
get方法
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
//占位节点 不放置数据 直接返回null
Node<K,V> find(int h, Object k) {
return null;
}
} public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//key的hash值
int h = spread(key.hashCode());
//数组桶不为空,从数组桶中取出对应hash的索引位置的节点
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;
}
//表示不是 node节点
//红黑树节点也在此查找
//可能是ForwardingNode、TreeBin、ReservationNode
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;
}
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);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
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;
//判断数组节点上的值是不是当前key
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
// 红黑树节点TreeBin 或者 ReservationNode 节点
return e.find(h, k);
}
//遍历链表
if ((e = e.next) == null)
return null;
}
}
}
}
//computeIfAbsent 和 compute 中使用的占位符节点
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
//占位节点 不放置数据 直接返回null
Node<K,V> find(int h, Object k) {
return null;
}
}
总结
- get方法没有使用锁 是弱一致性的 读线程可能无法立即观察到其他写线程的修改 因此可能读到已删除的数据或者无法实时读取添加的数据
- ForwardingNode节点得查找则去,转移得新数组桶中去查找
- ReservationNode节点则直接返回空
- TreeBin则是在红黑树中查找对应节点
remove和replace方法
//将key节点的数据为cv的 替换为 value 如果结果值为 null,则删除
final V replaceNode(Object key, V value, Object cv) {
//获取hash值
int hash = spread(key.hashCode());
//自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//没有初始化 或者没有对应的key
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) { //加锁当前节点
if (tabAt(tab, i) == f) { //判断当前节点没有变
//node节点
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)))) {
V ev = e.val;
// cv == null 表示根据key删除或根据key 替换
// cv != null 表示根据 k,v删除 或 根据k,v替换
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
//value不为null表示替换
if (value != null)
e.val = value;
//删除 pred != null 表示是链表上的节点
else if (pred != null)
//删除e节点
pred.next = e.next;
else
//数组中的数据 直接替换
setTabAt(tab, i, e.next);
}
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) {
//Map找到了对应的节点
if (oldVal != null) {
//删除节点
if (value == null)
//减少Map数量
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
流程
- 和put逻辑大致相同
- 数组桶没有初始化,或者查找key对应得索引处为空直接返回null
- 如果该数组桶正在扩容,则协助扩容
- 如果是数据节点则需要对该节点进行加锁,加锁后操作
- 根据逻辑判断是删除还是替换
- 如果是删除则需要修改Map数量
初始化数组桶方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { //当数组还没有初始化
// 表示数组桶正在初始化
if ((sc = sizeCtl) < 0) //sizeCtl = -1表示数组正在初始化
Thread.yield(); // lost initialization race; just spin
// CAS的方式 把sizeCtl 从sc变为-1 只有一个线程可以成功
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //当前线程获得初始化线程数组的资格
try {
if ((tab = table) == null || tab.length == 0) { //再次判断当数组还没有初始化
//初始中sc(sizeCtl)表示容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//初始一个n长度的数组
table = tab = nt;
//sc = 0.75n
sc = n - (n >>> 2);
}
} finally {
//初始化成功 sizeCtl表示需要扩容大小 数组桶长度的 0.75倍
sizeCtl = sc;
}
break;
}
}
return tab;
}
总结
- 使用CAS方式同时只有一个线程可以初始化数组桶
- 初始化之前 先取到sizeCtl值,表示数组桶长度
- 开始初始化就是将sizeCtl设置为-1成功的线程
- 初始化后将sizeCtl设置为数组长度的0.75倍,表示扩容容量
size方法
ConcurrentHashMap的计算方式和LongAdder类的计算方式一致,使用一个基计数器和一个计数数组(2的n次方大小),根据分段锁的思想,当一个线程修改基计数器失败时,就会根据当前线程生成一个随机数并根据计数数组计算出该线程应该去修改数组中的哪个位置的数,并使用CAS修改当前节点值
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
//计数器包含一个baseCount和一个CounterCell数组(长度为2的n次方)
//每次计数先修改baseCount 当修改baseCount失败,线程会生成一个随机数,通过和CounterCell数组长度减一进行与 算出一个索引,通过该索引定位到 CounterCell数组
//操作数组对应节点的对象进行加或者减 所以 该Map的总数为 baseCount+CounterCell数组中每个节点的值之和
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;
}
addCount方法
增加Map数量的方法,当putVal或者删除值得时候,调用此方法对Map数量增加或者修改,主要看上半部分计数方法
//添加到计数中,如果表太小且尚未调整大小,则启动传输。如果已调整大小,则在工作可用时帮助执行传输。
//在转移后重新检查占用率,以查看是否已经需要再次调整大小,因为调整大小是滞后添加。
//x – 要添加的计数
//check – 如果 <0,则不检查调整大小,如果 <= 1,则仅在未争用时检查
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/*************************************Map计数******************************************************************/
//多个线程同时修改计数 如果没有出现过冲突则一直修改的base 如果出现了冲突创建了计数器数组则优先修改计数器数组
if ((as = counterCells) != null ||
//CAS修改baseCount 如果修改失败则去操作CounterCell数组
!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 || //计数器数组还未初始化(第一次出现多个线程修改基计数器失败)
//ThreadLocalRandom.getProbe() 获取一个随机数
(a = as[ThreadLocalRandom.getProbe() & m]) == null || //该线程对应索引的节点为空
//更新该线程对应索引的节点失败(uncontended = false)
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
//如果不需要检查调整大小 直接返回
if (check <= 1)
return;
//记录修改个数后当前Map的数量
s = sumCount();
}
/*************************************计数后进行Map扩容**********************************************************/
//只有一个线程可以初始化新数组 其他线程都是协助数据迁移
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//第一个线程进入条件map数量大于扩容阈值
//第二个线程开始执行扩容 sizeCtl为负数
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//扩容戳 和 n 有关
int rs = resizeStamp(n);
if (sc < 0) {
//不需要参与扩容的条件
// (sc >>> RESIZE_STAMP_SHIFT) != rs 数组的长度已经发生了变化 扩容完毕
// 初始化时 sc = rs + 2 表示有一个线程正在扩容 扩容结束 线程会将sc-1 如果 sc == rs + 1 表示扩容已经完成 此处应该为 sc == rs << RESIZE_STAMP_SHIFT + 1
// sc == rs + MAX_RESIZERS 表示扩容线程达到了最大值 此处应该为 sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS
// (nt = nextTable) == null 表明nextTable还未初始化完毕 需要等待初始化完毕后才能协助数据迁移
// transferIndex <= 0 表明需要迁移的桶已经瓜分完毕
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// sizeCtl+1 表示又多一个线程参与了扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//尝试帮助扩容
transfer(tab, nt);
}
// rs << RESIZE_STAMP_SHIFT rs左移16位 高16位变成了低16位
// 在扩容时 sizeCtl作为扩容状态使用 高16位表示扩容戳 低16位存储执行扩容的线程数 默认为 2表示有一个线程
// (rs << RESIZE_STAMP_SHIFT) + 2 +2表示低16位加2初始化为2 表示有一个线程
// 此时 sizeCtl为负数
// rs << 16 +1 表示的是扩容结束 所以初始化一个线程为rs << 16 +2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//成功后开始执行扩容 只有一个线程可以修改 sizeCtl 为负数
transfer(tab, null);
s = sumCount();
}
}
}
fullAddCount方法
-
当多个线程修改基计数器出现竞争修改失败
-
已经失败过创建了数组计数器
上面两种情况则会去修改数组计数器
- 当数组计数器未初始化 uncontended = true
- 计数器已经初始化但是当前线程计数的索引位置为null uncontended = true
- 计数器已经初始化但是CAS修改当前节点失败 uncontended = false
上面情况会调用此方法fullAddCount方法
// See LongAdder version for explanation
private final void fullAddCount(long x, boolean wasUncontended) {
int h;//当前线程生成的一个随机数 用于计算当前线程要修改的数组计数器索引
//wasUncontended = true 标识当前线程的随机数已经是新的了
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // 强制初始化
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
//如果没有空的数组槽了 则为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) {
//cellsBusy 调整大小和/或创建 CounterCells 时使用的自旋锁(通过 CAS 锁定)。
//cellsBusy为1表示有线程正在初始化节点或初始化计数数组
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;
}
//wasUncontended为false表示在fullAddCount方法外已经进行过一次cas操作节点但是失败了,则重新生成随机数在获取一次索引
else if (!wasUncontended) // CAS already known to fail
//为true表示已经是新的随机值
wasUncontended = true; // Continue after rehash
//CAS操作节点值
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
//无法进行扩容 或者已经有线程进行了扩容 NCPU(CPU 数量,用于限制某些大小)
//1.counterCells != as表示counterCells已经变了(其他线程正在扩容)
//2.n >= NCPU 计数器数组数量大于或等于CPU数量也不会扩容
else if (counterCells != as || n >= NCPU)
collide = false; // At max size or stale
//表明下一次自旋要进行扩容。扩容会CAS操作节点值
else if (!collide)
collide = true;
//数组加锁进行扩容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { //加锁进行计数器数组扩容
try {
//as指针没有过期
if (counterCells == as) {// Expand table unless stale
//新建一个2倍长度的数组
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) {
//初始化一个长度为2的数组
CounterCell[] rs = new CounterCell[2];
//计算索引并放入新值
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
/*************************************数组在初始化中 更新base值************************************/
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
总结
在多线程使用分段式锁进行计数时分为三个阶段
- 当计数器数组没有被初始化时,初始化计数器数组
- 在初始化计数器数组前需要获取cellsBusy锁
- 初始化计数器数组为长度为2的CounterCell数组
- 将本线程需要操作的值初始到数组中
- 释放锁 结束
- 当计数器数组已经被初始化,则根据线程的随机数获取对应的数组槽
- 对应的数组槽有值
- wasUncontended为false表示在fullAddCount方法外已经进行过一次cas操作节点但是失败了,则重新生成随机数在获取一次索引
- wasUncontended为true 则CAS方式去修改数组槽对应的节点,修改成功 结束
- CAS失败的则判断是否需要进行扩容(以下情况不进行扩容)
- 如果计数器数组正在扩容
- 计数器数组长度大于等于CPU个数
- 计数器数组扩容
- 获取cellsBusy锁
- 将数组长度扩大二倍
- 数据转移 元数据索引位置对应新数据索引位置
- 扩容完毕后 重新生成随机数 继续循环修改计数器数组
- 对应的数组槽无值
- 获取cellsBusy锁 初始化CounterCell节点
- 初始化CounterCell成功,将节点放入数组槽
- 释放锁 结束
- 对应的数组槽有值
- 当计数器数组正在被初始化,则CAS方式修改基计数器
扩容方法
数量增加后判断是否需要扩容
//添加到计数中,如果表太小且尚未调整大小,则启动传输。如果已调整大小,则在工作可用时帮助执行传输。
//在转移后重新检查占用率,以查看是否已经需要再次调整大小,因为调整大小是滞后添加。
//x – 要添加的计数
//check – 如果 <0,则不检查调整大小,如果 <= 1,则仅在未争用时检查
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
/*************************************Map计数******************************************************************/
//多个线程同时修改计数 如果没有出现过冲突则一直修改的base 如果出现了冲突创建了计数器数组则优先修改计数器数组
if ((as = counterCells) != null ||
//CAS修改baseCount 如果修改失败则去操作CounterCell数组
!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 || //计数器数组还未初始化(第一次出现多个线程修改基计数器失败)
//ThreadLocalRandom.getProbe() 获取一个随机数
(a = as[ThreadLocalRandom.getProbe() & m]) == null || //该线程对应索引的节点为空
//更新该线程对应索引的节点失败(uncontended = false)
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
//如果不需要检查调整大小 直接返回
if (check <= 1)
return;
//记录修改个数后当前Map的数量
s = sumCount();
}
/*************************************计数后进行Map扩容**********************************************************/
//只有一个线程可以初始化新数组 其他线程都是协助数据迁移
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//1.map数量大于扩容阈值
//2.sizeCtl为负数
//3.SIZECTL表示扩容状态 高16位保存着扩容戳,低16位表示并发扩容的线程数
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
/**
* 每次新的扩容都会生成一个新的与n相关的扩容戳
*/
int rs = resizeStamp(n);
//表示当前数组桶正在扩容
if (sc < 0) {
/**不需要参与扩容的条件
* 1.(sc >>> RESIZE_STAMP_SHIFT) != rs 数组的长度已经发生了变化 扩容完毕
* 2.初始化时 sc = rs + 2 表示有一个线程正在扩容 扩容结束 线程会将sc-1 如果 sc == rs + 1 表示扩容 * 已经完成 (此处应该为 sc == rs << RESIZE_STAMP_SHIFT + 1)
* 3.sc == rs + MAX_RESIZERS 表示扩容线程达到了最大值
* 此处应该为 sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS
* 4.(nt = nextTable) == null 表明nextTable还未初始化完毕 需要等待初始化完毕后才能协助数据迁移
* 5.private transient volatile int transferIndex; 表示调整大小时要拆分的下一个表索引
* transferIndex <= 0 表明需要迁移的桶已经瓜分完毕
*/
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// sizeCtl+1 表示又多一个线程参与了扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
//尝试帮助扩容
transfer(tab, nt);
}
/**
* 1.将sizeCtl设置为 (rs << RESIZE_STAMP_SHIFT) + 2
* 2.RESIZE_STAMP_SHIFT = 16
* 3.rs << RESIZE_STAMP_SHIFT rs左移16位 高16位变成了低16位
* 4.在扩容时 sizeCtl作为扩容状态使用 高16位表示扩容戳 低16位存储执行扩容的线程数 默认为 2表示有一个线程
* 5.此时 sizeCtl为负数
* 6.rs << 16 +1 表示的是扩容结束 所以初始化一个线程为rs << 16 +2
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//成功后开始执行扩容 只有一个线程可以修改 sizeCtl 为负数
transfer(tab, null);
s = sumCount();
}
}
}
static final int resizeStamp(int n) {
// Integer.numberOfLeadingZeros(n) 返回n位二进制串从左算起连续的0的总数量
// (返回指定 int 值的二进制补码二进制表示形式中最高阶(“最左”)一位之前的零位数)
// 2的1次方 < n < 2的30次方 (30 ~ 1)00000000 00000000 00000000 00011110 ~ 00000000 00000000 00000000 00000001
// 结果范围 00000000 00000000 10000000 00000001 ~ 00000000 00000000 10000000 00011110
// 1 << (RESIZE_STAMP_BITS - 1) = 00000000 00000000 10000000 00000000
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
总结
- 在修改完Map的数量后,会判断当前Map是否需要扩容,当数量达到扩容阈值并且小于最大值则需要进行扩容
- 扩容前会根据当前数组桶的长度n计算一个扩容戳,每一个n计算的扩容戳都是不同的
- 在扩容中 sizeCtl 会保存当前扩容状态 高16位记录当前扩容戳信息 低16位记录当前扩容的线程数
- 在扩容中 sizeCtl 为 负数
- 第一次扩容会将 sizeCtl 设为 (rs << RESIZE_STAMP_SHIFT) + 2
transfer方法
扩容时 相当于把旧数组按stride分组,每个线程处理一组数组桶
transferIndex标记下一次分组的起始 从旧数据长度开始 初始时分组从n开始 第二次 为 n-stride
当一个桶位置迁移完毕 会使用ForwardingNode对象表示当前节点已经迁移完毕
最后完成迁移任务的线程检查整张表是否全部被替换为ForwardingNode(是线程安全的)
再确认整张表都迁移完成后 将table指向nextTable 并将nextTable置为空 sizeCtl设置为 0.75*n
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
/******************************确定步长******************************************************/
/**
* 1.stride表示步长 表示数据在迁移时每个线程负责迁移table中桶的个数
* 2.NCPU为CPU的核心数 n为原来数组桶长度
* 3.cpu核心数为1时 只有一个线程参与实际扩容
* 4.MIN_TRANSFER_STRIDE = 16 表示最小的步长
* 5.stride最小为16 当数组桶长度小于或等于16时 只有一个线程参与实际扩容
* 6.多核CPU 同时参与实际扩容的最大线程数为 8*NCPU
*/
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
//参与扩容的线程每次最少迁移16个节点
stride = MIN_TRANSFER_STRIDE; // subdivide range
/******************************第一次扩容新的数组桶为null需要初始化 只有一个线程初始化******************/
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //扩容为原来的2倍
// private transient volatile Node<K,V>[] nextTable; 要使用的下一张表;仅在调整大小时为 non-null。
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME 处理内存溢出
sizeCtl = Integer.MAX_VALUE;
return;
}
//新数组初始完毕
nextTable = nextTab;
//transferIndex表示table迁移时 table中下一个分配迁移任务的起始位置 从旧数组长度开始
transferIndex = n;
}
//新的数组长度
int nextn = nextTab.length;
//新建一个 fwd在旧数组的一个节点迁移完成后标记为 fwd hash固定值为 -1
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//advance 标志table[i]位置的迁移是否完成
boolean advance = true;
//标识 table整张表位置是否完成 最后一个线程将finishing设置为true 并进行收尾工作
boolean finishing = false; // to ensure sweep before committing nextTab
/******************************线程确定同步区间,开始同步数据*********************************/
/**
* 1.新的数组桶已经完成初始化,需要为每个线程分配区间进行数据转移
* 2.i表示要同步的节点索引 bound表示该线程最后要同步的索引位置
* 3.当i为负数表示整个数组桶都已经分配完毕
*/
for (int i = 0, bound = 0;;) { //自旋
Node<K,V> f; int fh;
/**
* 1.定位线程本轮处理的桶区间(bound~i)
* 2.当新一轮同步完成后发现还有数据没有同步继续下一轮直到数据同步完成
* 3.--i < bound表示当前线程分配的区间已经完成
* 4.transferIndex<=0该表的桶已被分配完毕
*/
while (advance) {
int nextIndex, nextBound;
// --i下一个待迁移的桶索引 --i < bound 表示已经迁移完分配的桶区间
if (--i >= bound || finishing)
advance = false;
//transferIndex<=0该表的桶已被分配完毕
else if ((nextIndex = transferIndex) <= 0) {
i = -1; //整个数组桶都已经分配完毕
advance = false;
}
// cas分配当前线程需要转移的区间并更新下一次开始同步的索引位置 bound~i(i>bound)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1; //transferIndex表示数组的长度 所以最后一个索引需要-1
advance = false;
}
}
/**********************当前线程分配的区间已经完成**************************/
// i<0说明所有桶区间已经被分配完毕 并且当前线程迁移完成自己负责的区间
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//迁移完成
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //扩容阈值变为 0.75 * 2n
return;
}
//数组桶迁移完成当前线程迁移完成自己负责的区间 sizeCtl 变为 扩容线程数 -1 sc-1
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果不是最后一个线程则直接跳出循环
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//最后一个线程 设置为 finishing和advance为true
finishing = advance = true;
//结束前检查一次
i = n; // recheck before commit
}
}
/**********************线程同步节点的过程****************************************/
//i>0 迁移过程 旧桶节点为null 直接放入fwd
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//旧桶节点已经为fwd直接设为true
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//加锁当前节点
synchronized (f) {
//节点没有变
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//Node节点 迁移逻辑和HashMap相同
if (fh >= 0) {
//计算hash前一位 是0还是1
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;
}
}
}
}
}
}
总结
扩容步骤
- 确定每个线程需要同步的节点数(stride)
- cpu核心数为1时 只有一个线程参与实际扩容
- 多核CPU 同时参与实际扩容的最大线程数为 8*NCPU
- stride最小为16 当数组桶长度小于或等于16时 只有一个线程参与实际扩容
- 初始化新的数组桶
- 当第一个线程进行扩容的时候进行初始化
- 其余辅助迁移的线程都会传入已经初始化好的新数组桶
- 如果新的数组桶没有初始化完成则不会进行辅助迁移
- 新的数组桶长度为原来的2倍
- 进行数据迁移,迁移完成的旧数据节点使用ForwardingNode替代
- 按照步长分段迁移每个线程迁移一段直到整个数组桶迁移完成