一、ConcurrentHashMap在JDK1.7和JDK1.8的区别
1.实现上的区别
在JDK1.7中采用的是Segment+HashEntry+Unsafe的实现
在JDK1.8中采用的是synchronized+CAS+Node+Unsafe的实现
ConcurrentHashMap在JDK1.7和1.8的区别:
(1)从结构上看,在JDK1.8中,ConcurrentHashMap的结构与HashMap基本类似
(2)从1.7到1.8的版本中,由于HashEntry从链表变成了红黑树,所以ConcurrentHashMap的时间复杂度从O(n)变成了O(log(n))
(3)HashEntry在1.8中称为Node,链表转红黑树的值是8,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构。不过这里链表长度超过8的时候,桶的数量必须要大于64。而当红黑树的数量小于6的时候,又会转换为链表
(4)在JDK1.7采用的是分段锁的概念,即Segment都是继承自ReentrantLock(可重入锁),但是在JDK1.8中采用的是synchronized+CAS的做法
使用CAS+synchronized替代分段锁的原因:
- 粒度降低了;
- JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
- 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。
ConcurrentHashMap的并发度
程序运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数。默认是16,且在构造器中可以设置。
当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
/**
* 得到一个大于c,且最接近c的2的幂次方值
*/
private static final int tableSizeFor(int c) {
// 对c-1得到n,然后每一次都对n先做或操作,再做无符号右移操作
// 因为int类型为32位,所以计算到16就全部结束了,最终得到的就是最高位及其以下的都为1,
// 这样就能保证得到的结果肯定大于或等于原来的n且为奇数,最后再加上1
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
在ConcurrentHashMap的构造器中,其并发度sizeCtl的设置,主要是通过tableSizeFor来进行,分析上面的源码,就可以知道为什么设置的时候,如果是17,其并发度其实是32。
这里的并发度,其实就是ConcurrentHashMap的table数组的数量,即有多少个链表。简单理解的话
CAS(compare and swap)
CAS是采用的非阻塞同步这样的乐观策略。
CAS指令需要有3个操作数:内存位置、旧的预期值、和新值。
CAS执行指令时,首先会从内存中取出旧值,接着通过计算获取到一个新值,再比较之前取出的旧值是否与内存中的该变量值一致,如果一致,将旧值swap为新值。如果之前取出的旧的预期值与内存中该变量的值不一致,则不执行更新操作,并且会重新获取旧值,在进行一次CAS指令操作。不过CAS不管是否执行更新操作,都会返回旧值。
因为使用悲观锁,需要频繁的进行上下文切换,而上下文切换很消耗性能,一次上下文切换大概会消耗3-5ms,而执行一条指令的时间大概只要0.6ns
线程一次拿锁的过程,至少是执行了两次上下文切换。这是因为悲观锁(synchronized)线程会主动进入阻塞状态,进入阻塞状态,那么要切换回来,就需要把执行状态的线程切换成其他状态,而把阻塞状态的切换成执行状态,这就两次切换;而乐观锁中线程并不会主动进入阻塞状态。
CAS的问题:
ABA问题:其实就是比如:线程1在比较之前,线程2把变量值变成了其他值,又变回来了,这个时候线程1去比较的时候,这个值依然是原来的,所以线程1会认为这个值没有变化过。但是实际上这个值已经被修改过。
开销问题:当比较的时候值一直不相等,那么会一直再来一次,当一个线程自旋,长期的不成功,对CPU的性能开销是很大的。
只能保证一个共享变量的原子操作:因为CAS是要比较内存中某个变量的值,一个地址保存一个变量,这样的话,CAS就无法比较多个变量的值,无法同时修改,想要同时的话,就需要多次的CAS,也就需要多次的自旋,CAS就不大能适应。
二、基于JDK1.8源码解析
1.常用的常量解析
/**
* 定义的最大表容量,由于32位hash值高两位用于控制的,所以是30位。2的30次方
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的初始table表容量,这里为16,这个是必须是2的幂次方
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 最大可能的table数组大小,toArray和其相关方法需要这个值
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 默认的并发级别,其实这个与DEFAULT_CAPACITY类似
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* 负载因子,默认是0.75
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 链表转换为红黑二叉树的最小临界点,当链表长度大于8时,链表转换为红黑二叉树
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 将红黑二叉树转换为链表的临街点,必须要小于TREEIFY_THRESHOLD
* 并且这个值最多是6的时候就需要转换为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 对table capacity进行红黑二叉树化的最小容量,该值应至少为4*TREEIFY_THRESHOLD,
* 以避免调整大小和树化阈值之间的冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 扩容线程每次最少要迁移的桶数量
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* sizeCtl中用于生成标记的位数。 对于32位阵列,必须至少为6。
*/
private static final int RESIZE_STAMP_BITS = 16;
/**
* 可以帮助调整大小的最大线程数。 必须为32-RESIZE_STAMP_BITS位。
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* 在sizeCtl中记录大小标记的位移位。
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 树根节点的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
*当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
*当为0时:代表当时的table还没有被初始化
*当为正数时:表示初始化或者下一次进行扩容的大小,这个值是table.length*0.75
*/
private transient volatile int sizeCtl;
2.table的初始化
private final Node[] initTable() {
Node[] tab; int sc;
//每次循环都获取最新的Node数组引用
while ((tab = table) == null || tab.length == 0) {
//sizeCtl是一个标记位,若为-1也就是小于0,代表有线程在进行初始化工作了
if ((sc = sizeCtl) < 0)
//让出CPU时间片
Thread.yield(); // lost initialization race; just spin
//CAS操作,将本实例的sizeCtl变量设置为-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//如果CAS操作成功了,代表本线程将负责初始化工作
try {
//再检查一遍数组是否为空
if ((tab = table) == null || tab.length == 0) {
//在初始化Map时,sizeCtl代表数组大小,默认16
//所以此时n默认为16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//Node数组
Node[] nt = (Node[])new Node[n];
//将其赋值给table变量
table = tab = nt;
//通过位运算,n减去n二进制右移2位,相当于乘以0.75
//例如16经过运算为12,与乘0.75一样,只不过位运算更快
sc = n - (n >>> 2);
}
} finally {
//将计算后的sc(12)直接赋值给sizeCtl,表示达到12长度就扩容
//由于这里只会有一个线程在执行,直接赋值即可,没有线程安全问题
//只需要保证可见性
sizeCtl = sc;
}
break;
}
}
return tab;
}
在这里,table使用了volatile关键字修饰,用来保证每次获取到的都是最新的数组,不能保证写入的是最新的值,想要保证是最新的值,其实是通过Node来保证。
sizeCtl也是一个volatile变量,它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。
CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功
另外,因为table使用volatile修饰,只能保证该数组的地址是volatile的,值不能被保证,所以table中的值其实是通过Node中的val和next来保证的
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
Node(int hash, K key, V val, Node next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString() {
return Helpers.mapEntryToString(key, val);
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node find(int h, Object k) {
Node e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
在ConcurrentHashMap被初始化的时候,并不会调用initTable初始化table数组,而initTable数组其实是在第一次调用putVal的时候进行初始化的。
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[] tab = table;;) {
Node f; int n, i, fh;
//判断Node数组为空
if (tab == null || (n = tab.length) == 0)
//初始化Node数组
tab = initTable();
...
}
3.ConcurrentHashMap.put()方法
其实就是调用的putVal()方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//对key的hashCode进行散列
int hash = spread(key.hashCode());
int binCount = 0;
//一个无限循环,直到put操作完成后退出循环
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//当Node数组为空时进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//Unsafe类volatile的方式取出hashCode散列后通过与运算得出的Node数组下标值对应的Node对象
//此时的Node对象若为空,则代表还未有线程对此Node进行插入操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//直接CAS方式插入数据(往tab数组插入数据的时候)
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
//插入成功,退出循环
break; // no lock when adding to empty bin
}
//查看是否在扩容,先不看,扩容再介绍
else if ((fh = f.hash) == MOVED)
//帮助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//对Node对象进行加锁
synchronized (f) {
//二次确认此Node对象还是原来的那一个,如果不是,则不执行if语句,那么synchronized中的内容就全部不会执行
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//无限循环,直到完成put
for (Node e = f;; ++binCount) {
K ek;
//和HashMap一样,先比较hash,再比较key,如果key的地址相等或者值相等则表示已经存在
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
// 节点不存在,其实就是添加在链表的末尾
if ((e = e.next) == null) {
//和链表头Node节点不冲突,就将其初始化为新Node作为上一个Node节点的next
//形成链表结构
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
// 如果是红黑树节点
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 如果链表的长度大于等于最小转换红黑树的节点个数即8个
// 将给定的节点索引替换所有链表中的节点
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
这里首先需要注意的是hash的计算,其实采用的是与HashMap一样的方式,都是通过key的hashCode的高16位异或低16位实现的。这样做的目的是保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
// 获取hash,这里的h是传入key.hashCode()
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
其次需要注意的是tabAt(tab, i)方法,其使用了Unsafe类volatile的操作volatile式的查看值,用以保证每次获取的值都是最新的。这是因为Unsafe可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。
static final Node tabAt(Node[] tab, int i) {
return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
虽然上面的table变量加了volatile,但也只能保证其引用的可见性,并不能确保其数组中的对象是否是最新的,所以需要Unsafe类volatile式地拿到最新的Node
索引定位:
// n为桶的个数,即table的长度
(n - 1) & hash
put方法总结:
如果没有初始化就先调用initTable()方法来进行初始化过程,其中sizeCtl是标志位,初始由CAS操作将之置为-1,表示正在初始化
如果没有hash冲突就直接CAS插入
如果还在进行扩容操作就先进行扩容
如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环(阿里面试官问题,默认的链表大小,超过了这个值就会转换为红黑树);
如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
4.ConcurrentHashMap.get()
//会发现源码中没有一处加了锁
public V get(Object key) {
Node[] tab; Node 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) {//读取首节点的Node元素
if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
5.ConcurrentHashMap扩容
如果新增节点之后,所在的链表的元素个数大于等于8,则会调用treeifyBin把链表转换为红黑树。在转换table结构时,即treeifyBin,若table的长度小于MIN_TREEIFY_CAPACITY=64,则会将数组的长度扩大到原来的两倍,并触发transfer,重新调整节点位置。(只有当tab.length >= 64, ConcurrentHashMap才会使用红黑树。)具体可以看putVal中关于binCount最后的判断,调用treeifyBin方法,其内部的处理
新增节点后,addCount统计tab中的节点个数大于阈值(sizeCtl),会触发transfer,重新调整节点位置。
(1)ConcurrentHashMap.addCount
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//首先if判断counterCells为空并且对当前map对象cas操作baseCount + x成功,就跳过if里的操作,
//因为都cas操作baseCount + x成功了,就不需要通过counterCells辅助了,简单明了。
// 如果counterCells为null或者baseCount+x的cas操作失败
// 则进入if条件
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//如果counterCells为空,直接执行fullAddCount。
//如果不为空,判断当前线程在counterCells中的槽位是否为空,如果不为空,
//对槽位中的CounterCell对象cas操作value加1,成功return,失败执行fullAddCount,如果槽位为空,直接执行fullAddCount
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// cas操作失败,则会执行fullAddCount方法,cas主要是多线程操作的时候
// 操作失败的线程会执行fullAddCount。
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 检查是否扩容
if (check >= 0) {
Node[] tab, nt; int n, sc;
// 节点个数大于阈值sizeCtl,并且table为不null,table的长度小于最大的容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 调整table的长度n,用于标识当前的size。
//生成一个戳 算法是Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
//如果n=16,则生成的值为0000000000000000010000000000011011(27和2的15次方相或)
int rs = resizeStamp(n);
// sc<0其实就是sizeCtl<0,表示正在进行扩容
if (sc < 0) {
// 其他线程正在进行扩容时,break
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
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();
}
}
}
(2)ConcurrentHashMap.transfer
// 复制元素到nextTab
private final void transfer(Node[] tab, Node[] nextTab) {
int n = tab.length, stride;
// 在这里NCPU是CPU的核心数,每个CPU核心平均分配复制任务
// stride就表示每个线程进来时需要复制的量,如果stride小于最小复制的大小即16,则赋值为16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// nextTab为null,初始化nextTab,先对原table的基础上扩容一倍
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// 记录nextTable,用于下一次resize
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
// 构造一个forward节点,用于存放不需要复制的节点
ForwardingNode fwd = new ForwardingNode(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//通过for自循环处理每个槽位中的链表元素,默认advace为真,
//通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号,
//bound指需要处理的槽位边界,即下限,这里是先处理槽位15的节点;
// 无限for循环,i和bound的值在while循环中处理得到
// for循环的结束,由if (i < 0 || i >= n || i + n >= nextn) 决定,满足该条件才结束for循环的复制过程
for (int i = 0, bound = 0;;) {
Node f; int fh;
while (advance) {
int nextIndex, nextBound;
// --i是转移表
// 比如第一次while循环,i=0,bound=0,transferIndex=tab.length
// 所以前面两个if条件都不会满足,直接进入第三个if条件,cas操作
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 比较this对象中TRANSFERINDEX偏移量位置的值与nextIndex做比较
// 如果相等,就把nextBound赋值给TRANSFERINDEX偏移量位置的值
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//第一次进入的时候如果到达了这里则接到了当前应该进行的任务:负责搬运[nextBound:i]之间的内容
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 线程执行到这里,table复制过程结束
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果所有的节点都已经完成复制工作 就把nextTable赋值给table 清空临时对象nextTable
if (finishing) {
nextTable = null;
table = nextTab;
//扩容阈值设置为原来容量的1.5倍 依然相当于现在容量的0.75倍。因为扩容因子是0.75
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);
// 如果遍历到了forward节点,说明已经被处理过了,则跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// 具体的搬运复制过程
else {
// 针对Node链表添加锁,这里做了双重检索
synchronized (f) {
if (tabAt(tab, i) == f) {
Node ln, hn;
// 链表节点
if (fh >= 0) {
// resize后的元素要么在原地,要么移动n位(n为原capacity)
// 对fh链表节点做与操作
int runBit = fh & n;
Node lastRun = f;
for (Node 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 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(ph, pk, pv, ln);
else
hn = new Node(ph, pk, pv, hn);
}
// 在nextTab的i位置插入一个链表
setTabAt(nextTab, i, ln);
// 在nextTab的i+n位置插入一个链表
setTabAt(nextTab, i + n, hn);
// 将tab中的i位置置为fwd,则tab中i位置的节点已经被复制
setTabAt(tab, i, fwd);
// 置为true,继续下一次for循环,并且在最初的while执行
advance = true;
}
// 如果f是树形节点
else if (f instanceof TreeBin) {
TreeBin t = (TreeBin)f;
TreeNode lo = null, loTail = null;
TreeNode hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode p = new TreeNode
(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;
}
}
//(1)如果lo链表的元素个数小于等于UNTREEIFY_THRESHOLD,默认为6,
// 则通过untreeify方法把树节点链表转化成普通节点链表;
//(2)否则判断hi链表中的元素个数是否等于0:如果等于0,表示lo链表中包含了所有原始节点,
// 则设置原始红黑树给ln,否则根据lo链表重新构造红黑树。
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
6.ConcurrentHashMap统计个数
ConcurrentHashMap中的元素个数,是由baseCount和CounterCell数组中每个元素的value值之和得到的。这样做的原因是,当多个线程同时执行CAS修改baseCount值,失败的线程会将值放到CounterCell中。
/**
* 基本计数器值,主要在没有争用时使用。 通过CAS更新。
*/
private transient volatile long baseCount;
/**
* table计数单元数组,大小是2的次幂
*/
private transient volatile CounterCell[] counterCells;
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
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;
}
三、参考
ConcurrentHashMap是如何实现线程安全的