底层数据结构:
- 底层实现和HashMap是一致的,各种特性也是基本保持一致。
与1.7版本的区别:
- 取消了segment的分段设置,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据加锁进一步减少并发冲突的概率。
- 将数组和单项链表的机构变成了数组+单向链表+红黑树结构。
成员变量:
/**********************************************HashMap中也有的*******************************************************************/
/**
* 最大的容量
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的初始化容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 扩容因子
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 当链表的长度大于8会转化成红黑树的点1
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当红黑树的节点个数小于6的时候转化成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 数组的最大值
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 只有当容器中的元素的数量达到64的时候才可以转化成红黑树
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 默认的初始化数组 用来保存元素
*/
transient volatile Node<K, V>[] table;
/**********************************************HashMap中也有的*******************************************************************/
/**********************************************HashMap中没有的*******************************************************************/
/**
* 用来控制表初始化和扩容的
* <p>
* 1,默认值为0,
* 2,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
* 3,当为负的时候,说明表正在初始化或扩张,
* <p>
* -1:表示初始化
* -(1+n) n:表示活动的扩张线程
* 0:默认值,后续在真正初始化的时候使用默认容量
* > 0:初始化或扩容完成后下一次的扩容门槛
*/
private transient volatile int sizeCtl;
/**
* 转移的时候用的数组
*/
private transient volatile Node<K, V>[] nextTable;
/**
* 默认并发数 Segment 分段锁 默认的段 未被使用 此表的默认并发级别。未使用但是 定义为与此类的先前版本兼容。
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 用于记录元素的个数
*/
private transient volatile long baseCount;
/**
* 计数器表。当非null时,size是2的幂。
* counterCells是一个元素为CounterCell的数组,该数组的大小与当前机器的CPU数量有关,并且它不会被主动初始化,只有在调用fullAddCount()函数时才会进行初始化。
*/
private transient volatile CounterCell[] counterCells;
// views
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;
// 扩容时使用的的索引
private transient volatile int transferIndex;
// 旋转锁
private transient volatile int cellsBusy;
//每次进行转移的最小值这个值作为一个下限来避免Rsisize遇到过多的内存争用
private static final int MIN_TRANSFER_STRIDE = 16;
// 生成sizeCtl所使用的bit位数
private static final 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;
static final int HASH_BITS = 0x7fffffff; // 正常节点哈希的可用位 1111111111111111111111111111111
// 获取可用的CPU个数
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 进行序列化的属性
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE),
};
/**********************************************Unsafe*******************************************************************/
// 在ConcurrentHashMap中使用了unSafe方法,通过直接操作内存的方式来保证并发处理的安全性,使用的是硬件的安全机制
private static final Unsafe U = Unsafe.getUnsafe();
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final int ABASE;
private static final int ASHIFT;
static {
SIZECTL = U.objectFieldOffset
(ConcurrentHashMap.class, "sizeCtl");
TRANSFERINDEX = U.objectFieldOffset
(ConcurrentHashMap.class, "transferIndex");
BASECOUNT = U.objectFieldOffset
(ConcurrentHashMap.class, "baseCount");
CELLSBUSY = U.objectFieldOffset
(ConcurrentHashMap.class, "cellsBusy");
CELLVALUE = U.objectFieldOffset
(CounterCell.class, "value");
ABASE = U.arrayBaseOffset(Node[].class);
int scale = U.arrayIndexScale(Node[].class);
if ((scale & (scale - 1)) != 0)
throw new ExceptionInInitializerError("array index scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
Class<?> ensureLoaded = LockSupport.class;
ensureLoaded = ReservationNode.class;
}
/**********************************************Unsafe*******************************************************************/
/**********************************************HashMap中没有的*******************************************************************/
- sizeCtl(重点):
用来控制表初始化和扩容的 -1:表示初始化 -(1+n) n:表示活动的扩张线程 0:默认值,后续在真正初始化的时候使用默认容量 > 0:初始化或扩容完成后下一次的扩容门槛
三个原子操作的方法
/*******************************【 三个原子的方法 】**************************/
/**
* 返回数组指定位置的的节点原子操作
*/
@SuppressWarnings("unchecked")
static final <K, V> Node<K, V> tabAt(Node<K, V>[] tab, int i) {
return (Node<K, V>) U.getObjectAcquire(tab, ((long) i << ASHIFT) + ABASE);
}
/**
* 在指定位置设置值
* tab 要修改的数组
* i 要设置的节点的索引值
* c 期望值
* v 修改的值
* expcession:
* 利用CAS算法设置i位置上的Node节点。
* 当且仅当tab[i]=c的时候 才修改值为v
*
*/
static final <K, V> boolean casTabAt(Node<K, V>[] tab, int i, Node<K, V> c, Node<K, V> v) {
return U.compareAndSetObject(tab, ((long) i << ASHIFT) + ABASE, c, v);
}
/**
* 在指定位置设置值
* 利用volatile方法设置节点位置的值
*/
static final <K, V> void setTabAt(Node<K, V>[] tab, int i, Node<K, V> v) {
U.putObjectRelease(tab, ((long) i << ASHIFT) + ABASE, v);
}
/*******************************【 三个原子的方法 】**************************/
- 在集合中出现频率高的一匹
构造方法
/************************************** 【构造方法】 *************************************************/
/**
* 默认的构造方法
*/
public ConcurrentHashMap() {
}
/**
* 这里多了一个参数
* 传入初始化参数
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1); /**传入了扩容因子*/
}
/**
* @param initialCapacity 初始化容量参数
* @param loadFactor 默认的扩容因子
* @param concurrencyLevel 初始化的时候传入1
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)// 默认的负载因子小于等于0则抛出异常
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
long size = (long) (1.0 + (long) initialCapacity / loadFactor);
int cap = (size >= (long) MAXIMUM_CAPACITY) ? // size 大于最大容量?
MAXIMUM_CAPACITY : // 设置为最大容量
tableSizeFor((int) size); // 返回传入容量的最接近2的n次方的最大值(带扩容的 比如说传入13 得到的值是32 )
this.sizeCtl = cap; // 设置sizeCtl的值 也就是扩容门槛(设置的就是最大的扩容门槛也就是当前最接近传入的容量的向上取2次方)
}
/************************************** 【构造方法】 *************************************************/
- sizeCtl 的值在这里设置的是当前传入的容量向上取直接近的2的次幂
Put方法解析
/**
* 放置元素到集合中 调用的是下面的这个put方法
*/
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()); // 计算hash值 得出来的结果和hashMap的是一样的 低位和高位相异或
int binCount = 0; // 要插入的元素所在桶的元素个数
for (Node<K, V>[] tab = table; ; ) { //死循环 几乎覆盖整合方法的大循环 结合CAS使用(如果CAS失败,则会重新取整个桶进行下面的流程)
Node<K, V> f;int n, i, fh; K fk;V fv;
if (tab == null || (n = tab.length) == 0) //tab未经历过初始化
tab = initTable(); // fixme 初始化 《1》
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置
if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value))) // 当tab[i]=null 的之后设置值为【new Node<K, V>(hash, key, value)】
break; //插入成功跳出循环
}
else if ((fh = f.hash) == MOVED) // 当前元素的所在的桶的第一个元素的值为MOVED 则当前线程帮忙一起迁移元素 MOVED表示正在扩容过程中
tab = helpTransfer(tab, f);//fixme 帮忙迁移元素(正在扩容的过程中)《2》
else if (onlyIfAbsent && fh == hash && ((fk = f.key) == key || (fk != null && key.equals(fk))) && (fv = f.val) != null) return fv; // 这个暂时没看懂
else { // 不是初始化 而且当前hash值得链表的地方有值 而且当前桶不是扩容状态
V oldVal = null;
synchronized (f) { // 给链表中的第一个元素加锁
if (tabAt(tab, i) == f) { // 再次检测第一个元素是否有变化,如果有变化则进入下一次循环,从头来过
// 链表
if (fh >= 0) {
binCount = 1; // 如果是链表那么 做为循环次数初始化值赋值为1
for (Node<K, V> e = f; ; ++binCount) { // 遍历整个桶,每次结束binCount加1
K ek;
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);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) { // 如果第一个元素是树节点
Node<K, V> p;
binCount = 2; // 如果是树直接赋值为2
if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) { //调用红黑树的插入方法插入元素,如果成功插入则返回null,否则返回寻找到的节点
oldVal = p.val;// 如果找到了这个元素,则赋值了新值
if (!onlyIfAbsent)
p.val = value;
}
} else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) { // 如果binCount不为0,说明成功插入了元素或者寻找到了元素
if (binCount >= TREEIFY_THRESHOLD)// 如果链表元素个数达到了8
treeifyBin(tab, i);//尝试树化
if (oldVal != null)
return oldVal; // 如果要插入的元素已经存在,则返回旧值
break; // 退出外层大循环,流程结束
}
}
}
addCount(1L, binCount); // fixme 成功插入元素,元素个数加1(是否要扩容在这个里面) 《3》
return null; // 成功插入元素返回null
}
- 总结
- (1)如果桶数组未初始化,则初始化;
- (2)如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;
- (3)如果正在扩容,则当前线程一起加入到扩容的过程中;
- (4)如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);
- (5)如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;
- (6)如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
- (7)如果元素存在,则返回旧值;
- (8)如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容;
- 需要关注里面的三个方法
- (1) initTable(); 初始化方法
- (2) helpTransfer(tab, f); 帮助迁移元素
- (3) addCount(1L, binCount);插入元素并且检查是否需要扩容
initTable(); 初始化方法
private final Node<K, V>[] initTable() {
Node<K, V>[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) // 如果sizeCtl<0说明正在初始化或者扩容,让出CPU
Thread.yield();
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* @param obj 需要更新的对象
* @param offset obj中要更新的变量
* @param expect 期望值
* @param update 如果期望值expect与offset的当前值相同,设置offset的值为这个新值
* @return 如果field的值被更改返回true
* public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
*/
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { // 使用CAS锁控制只有一个线程初始化桶数组;
// 如果把sizeCtl原子更新为-1成功,则当前线程进入初始化
// 如果原子更新失败则说明有其它线程先一步进入初始化了,则进入下一次循环
// 如果下一次循环时还没初始化完毕,则sizeCtl<0进入上面if的逻辑让出CPU
// 如果下一次循环更新完毕了,则table.length!=0,退出循环
try {
if ((tab = table) == null || tab.length == 0) { // 再次检查table是否为空,防止ABA问题
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 如果sc为0则使用默认值16
@SuppressWarnings("unchecked")
Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n]; // 新建数组
table = tab = nt; // 赋值给table桶数组
sc = n - (n >>> 2); // 设置sc为数组长度的0.75倍 【 n - (n >>> 2) = n - n/4 = 0.75n 】 可见这里装载因子和扩容门槛都是写死了的 这也正是没有threshold和loadFactor属性的原因
}
} finally {
sizeCtl = sc;//把sc赋值给sizeCtl,这时存储的是扩容门槛,就是桶数组大小的0.75倍
}
break;
}
}
return tab;
}
- 总结:
- (1)使用CAS锁控制只有一个线程初始化桶数组;
- (2)sizeCtl在初始化后存储的是扩容门槛;
- (3)扩容门槛写死的是桶数组大小的0.75倍,桶数组大小即map的容量,也就是最多存储多少个元素。
- 关于CAS的介绍 compareAndSet :
- 存在三个值 当前值 预期值 新值 当且仅当当前值等于预期值才会把当前值更新为新值
- 什么是ABA问题?
- 假设当前值为A 一个线程把当前值A 改为B 又把B改为A 另一个线程用CAS来操作当前值还是成功的
helpTransfer(tab, f); 帮助迁移元素
final Node<K, V>[] helpTransfer(Node<K, V>[] tab, Node<K, V> f) {
Node<K, V>[] nextTab;
int sc;
// 如果桶数组不为空,并且当前桶第一个元素为ForwardingNode类型,并且nextTab不为空,说明当前桶已经迁移完毕了,才去帮忙迁移其它桶的元素, 扩
// 容时会把旧桶的第一个元素置为ForwardingNode,并让其nextTab指向新桶数组
if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K, V>) f).nextTable) != null) {
// 根据 length 得到一个标识符号
int rs = resizeStamp(tab.length);
//fixme sizeCtl<0,说明正在扩容 // 如果 nextTab 没有被并发修改 且 tab 也没有被并发修改 且 sizeCtl < 0 (说明还在扩容)
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
// 如果 sizeCtl 无符号右移 16 不等于 rs ( sc前 16 位如果不等于标识符,则标识符变化了)
// 或者 sizeCtl == rs + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 或者 sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
// 或者转移下标正在调整 (扩容结束)
// 结束循环,返回 table
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) { // 如果以上都不是, 将 sizeCtl + 1, (表示增加了一个线程帮助其扩容)
// 当前线程帮忙迁移元素
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
- 总结:
- (1) 如果当前链表已经迁移完毕,帮其他线程迁移元素
- (2) 迁移完成跳出循环,返回迁移完毕的新数组
- (3) 当前链表未迁移完毕,直接返回原数组
- 需要注意的方法
- transfer(tab, nextTab);真正扩容的方法 扩容时容量变为两倍,并把部分元素迁移到其它桶中。
addCount(1L, binCount);插入元素并且检查是否需要扩容
private final void addCount(long x, int check) {
CounterCell[] cs;
long b, s;
// 把数组的大小存储根据不同的线程存储不同的当前集合元素的数量,也是一种分段的思想 不过是放到了上面
// 尝试使用CAS更新baseCount失败
// CAS更新失败转用CounterCells进行更新
/**
* compareAndSwapObject(Object var1, long var2, Object var3, Object var4)
* var1 操作的对象
* var2 操作的对象属性
* var3 var2与var3比较,相等才更新
* var4 更新值
* 例子:
* UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
*/
if ((cs = counterCells) != null || !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) { // 如果计数盒子不是空 或者如果修改baseCount 失败
CounterCell c;
long v;
int m;
boolean uncontended = true;
// 如果计数盒子是空(尚未出现并发)
// 如果随机取余一个数组位置为空 或者
// 修改这个槽位的变量失败(出现并发了)
// 执行 fullAddCount 方法。并结束
if (cs == null || (m = cs.length - 1) < 0 || //如果cs为空 或者长度为0
(c = cs[ThreadLocalRandom.getProbe() & m]) == null || // 当前线程所拥有的cs为null
/*ThreadLocalRandom.getProbe() & m
每个线程都会通过ThreadLocalRandom.getProbe() & m寻址找到属于它的CounterCell,然后进行计数。
ThreadLocalRandom是一个线程私有的伪随机数生成器,每个线程的probe都是不同的,可以认为每个线程的probe就是它在CounterCell数组中的hash code。
这种方法将竞争数据按照线程的粒度进行分离,相比所有竞争线程对一个共享变量使用CAS不断尝试在性能上要效率多了,
这也是为什么在高并发环境下LongAdder要优于AtomicInteger的原因。
*/
!(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x)) //在当前线程的段上加数量失败
) {
// 强制增加数量(无论如何数量是一定要加上的,并不是简单地自旋)
// 说明已经发生冲突了,那么就对counterCells进行扩容,以减少多个线程hash到同一个段的概率
// 调用fullAddCount(),该函数负责初始化CounterCells和更新计数
fullAddCount(x, uncontended);
return;
}
// 这里check 可能在别的方法传入的时候传入了null
if (check <= 1)
return;
// 计算元素个数
s = sumCount();
}
// 如果需要检查,检查是否需要扩容,在 putVal 方法调用时,默认就是要检查的。
if (check >= 0) {
Node<K, V>[] tab, nt;
int n, sc;
// 如果元素个数达到了扩容门槛,则进行扩容
// 注意,正常情况下sizeCtl存储的是扩容门槛,即容量的0.75倍
while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
// 根据 length 得到一个标识
int rs = resizeStamp(n);
// 如果正在扩容
if (sc < 0) {
// 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
// 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
// 扩容已经完成了,退出循环 正常应该只会触发nextTable==null这个条件,其它条件没看出来何时触发
// 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
// 如果 nextTable == null(结束扩容了)
// 如果 transferIndex <= 0 (转移状态变化了)
// 结束循环
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
break;
// 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
// 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.
} else if (U.compareAndSetInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
// 这里是触发扩容的那个线程进入的地方
// sizeCtl的高16位存储着rs这个扩容邮戳
// sizeCtl的低16位存储着扩容线程数加1,即(1+nThreads)
// 进入迁移元素
transfer(tab, null);
// 重新计算元素个数
s = sumCount();
}
}
}
- 变量解释
- CounterCells数组:可以看成一个hash表结构(
每个线程都会通过ThreadLocalRandom.getProbe() & m寻址找到属于它的CounterCell,然后进行计数。 ThreadLocalRandom是一个线程私有的伪随机数生成器,每个线程的probe都是不同的,可以认为每个线程的probe就是它在CounterCell数组中的hash code,Value就是当前线程所存储的value的值)
- 总结
- (1)先尝试把数量加到baseCount上,如果失败再加到分段的CounterCell上
- (2)元素个数存储在不同的段上,减少不同线程同时更新size时的冲突;
- (3)计算元素个数时把这些段的值及baseCount相加算出总的元素个数;
- (4)检查是否需要扩容
- (4-1)如果正在扩容,当前线程帮忙一起扩容
- (4-2)如果没有在扩容,则进行扩容
- (4-3) 重新计算元素数量
transfer(tab, nextTab);真正扩容的方法 扩容时容量变为两倍,并迁移部分元素
private final void transfer(Node<K, V>[] tab, Node<K, V>[] nextTab) {
int n = tab.length, stride;
/***************************************【 初始化每个CPU要处理的链表的个数 】******************************************************************/
//这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象
stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;// 如果当前可同CPU的个数> 1 那么数组长度/8/cpu个数 如果cpu个数不大于1 那么1哥cpu处理全部的桶
if (stride< MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
/***************************************【 初始化每个CPU要处理的链表的个数完毕 】******************************************************************/
/***************************************【 初始化nextTab 】******************************************************************/
if (nextTab == null) { // 如果nextTab为空,说明还没开始迁移
try {
@SuppressWarnings("unchecked")
Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n << 1]; //就新建一个新桶数组 新桶数组是原桶的两倍
nextTab = nt; // 设置nextTab的值
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab; //更新成员变量
transferIndex = n; //更新转移下标,表示转移时的下标(最一开始是数组的长度)
}
/***************************************【 初始化nextTab完毕 】******************************************************************/
/***************************************【 初始化转移数组完毕 】******************************************************************/
// 新桶数组大小
int nextn = nextTab.length;
/*创建一个 fwd 节点,表示一个正在被迁移的 Node,并且它的 hash 值为-1(MOVED),也
//就是前面我们在讲 putval 方法的时候,会有一个判断 MOVED 的逻辑。它的作用是用来占位,表示
//原数组中位置 i 处的节点完成迁移以后,就会在 i 位置设置一个 fwd 来告诉其他线程这个位置已经
//处理过了,具体后续还会在讲*/
// 表示正在被迁移的node
ForwardingNode<K, V> fwd = new ForwardingNode<>(nextTab);
/***************************************【 初始化转移数组完毕 】******************************************************************/
// 当前线程能否继续处理下一个链表
boolean advance = true;
// 扩容完成状态
boolean finishing = false;
// i 表示下标, bound 表示当前线程可以处理的当前桶区间最小下标
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) { //扩容结束了,当前线程可以退出了 就是hash桶没了
i = -1;
advance = false;
//你处理完了啊 再给你新的任务 获取最新的转移下标再去给我转一个区间
} else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { // CAS 修改 transferIndex,即 length - 区间值,留下剩余的区间值供后面的线程使用
bound = nextBound; // 当前线程可处理的链表区间的最小下标
i = nextIndex - 1;// 第一次赋值赋值的是当前线程可处理的链表区间的最大下标
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) { //目前只发现i<0这个条件
int sc;
if (finishing) { // 扩容完毕
nextTable = null;
table = nextTab; // 设置新数组
sizeCtl = (n << 1) - (n >>> 1); // 并设置下一次扩容门槛为新桶数组容量的0.75倍
return;
}
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 当前线程扩容完成,把扩容线程数-1
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return;
// finishing为true才会走到上面的if条件
finishing = advance = true;
i = n;
}
} else if ((f = tabAt(tab, i)) == null)// 如果桶中无数据,直接放入ForwardingNode标记该桶已迁移
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED) // 如果桶中第一个元素的hash值为MOVED 说明它是ForwardingNode节点 也就是该桶已迁移
advance = true;
else {
synchronized (f) {// 锁定该桶并迁移元素
if (tabAt(tab, i) == f) { //再次判断当前桶第一个元素是否有修改,也就是可能其它线程先一步迁移了元素
// 规则是桶中各元素的hash与桶大小n进行与操作
// 等于0的放到低位链表(low)中,不等于0的放到高位链表(high)中
// 其中低位链表迁移到新桶中的位置相对旧桶不变 高位链表迁移到新桶中位置正好是其在旧桶的位置加n
Node<K, V> ln, hn; //把一个链表分化成两个链表
if (fh >= 0) { //第一个元素的hash值大于等于0,说明该桶中元素是以链表形式存储的, 这里与HashMap迁移算法基本类似
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) //把hash&n为0的放在低位链表中
ln = new Node<K, V>(ph, pk, pv, ln);
else //不为0的放在高位链表中
hn = new Node<K, V>(ph, pk, pv, hn);
}
// 低位链表的位置不变
setTabAt(nextTab, i, ln);
// 高位链表的位置是原位置加n
setTabAt(nextTab, i + n, hn);
// 标记当前桶已迁移
setTabAt(tab, i, fwd);
// advance为true,返回上面进行--i操作
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(JDK 1.8) putVal 源码分析写的就很详细啦
remove方法删除元素
/**
* 删除元素
*/
public V remove(Object key) {
// 调用替换节点方法
return replaceNode(key, null, 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;
if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) // 如果目标key所在的桶不存在,跳出循环返回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) {// 再次验证当前桶第一个元素是否被修改过
if (fh >= 0) { // 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;
if (cv == null || cv == ev || (ev != null && cv.equals(ev))) { // 检查目标节点旧value是否等于cv
oldVal = ev;
if (value != null) // 如果value不为空则替换旧值
e.val = value;
else if (pred != null) // 删除当前节点
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));
}
}
} else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (validated) { // 如果处理过,不管有没有找到元素都返回
if (oldVal != null) { // 如果处理过,不管有没有找到元素都返回
if (value == null)// 如果要替换的值为空,元素个数减1
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null; // 没找到元素返回空
}
- (1)计算hash;
- (2)如果所在的桶不存在,表示没有找到目标元素,返回;
- (3)如果正在扩容,则协助扩容完成后再进行删除操作;
- (4)如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除;
- (5)如果是以树形式存储的,则遍历树查找元素,找到之后再删除;
- (6)如果是以树形式存储的,删除元素之后树较小,则退化成链表;
- (7)如果确实删除了元素,则整个map元素个数减1,并返回旧值;
- (8)如果没有删除元素,则返回null;
get方法获取元素
/**
* 获取元素的方法
*/
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
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) // hash小于0,说明正在扩容
return (p = e.find(h, key)) != null ? p.val : null; // 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
while ((e = e.next) != null) { // 遍历整个链表寻找元素
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- (1)hash到元素所在的桶;
- (2)如果桶中第一个元素就是该找的元素,直接返回;
- (3)如果是树或者正在迁移元素,则调用各自Node子类的find()方法寻找元素;
- (4)如果是链表,遍历整个链表寻找元素;
- (5)获取元素没有加锁;
获取元素个数
/**
* 获取元素个数的方法
* @return
*/
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long) Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) n);
}
// 遍历CounterCell 线程中的数量 取得代表数量的value之后相加得出总数量 + sum(baseCount的值)
final long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (CounterCell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
- (1)元素的个数依据不同的线程存在在不同的段里;(见addCounter()分析)
- (2)计算CounterCell所有段及baseCount的数量之和;
- (3)获取元素个数没有加锁;
参考自:微信公共号彤哥读源码