一:简述
本文基于jdk1.8对concurrentHashMap的源码进行分析,以put()方法为入口对concurrentHashMap的扩容机制,size计算方式等代码进行分析
二:concurrentHashMap成员变量
//最大容量
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;
//将链表转化为红黑树的node数组长度阈值
static final int MIN_TREEIFY_CAPACITY = 64;
//默认的线程迁移数据范围
private static final int MIN_TRANSFER_STRIDE = 16;
//当前服务器cpu数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//真正存储数据的容器
transient volatile Node<K,V>[] table;
//用于扩容的新数组
private transient volatile Node<K,V>[] nextTable;
//和counterCells一起用于计算concurrentHashMap的size
private transient volatile long baseCount;
// -1的时候代表正在node数组正在初始化 初始化之后赋值为扩容的阈值
private transient volatile int sizeCtl;
//数据迁移的索引
private transient volatile int transferIndex;
//用于计算concurrentHashMap的size时需要加cas锁的标记
private transient volatile int cellsBusy;
//用于计算concurrentHashMap的size 默认长度是2
private transient volatile CounterCell[] counterCells;
三:concurrentHashMap的源码分析
以put方法为入口对源码进行分析
1.put方法流程图
2.源码分析
put()方法会调用putVal()方法,如果当前数组没有初始化那么会先调用initTable()方法初始化数组,然后根据计算好的数组下标查看当前下标下是否为null,如果是null,那么利用cas保证线程安全直接进行替换,如果不是null,那么需要解决hash冲突的问题,分链表和红黑树两种情况分别进行处理。
a.链表:遍历链表,如果有相同的key 进行覆盖的操作 否则添加到链表的尾部(尾插法)。
b.红黑树:遍历红黑树,如果有相同的key,进行覆盖操作,如果没有,那么构建红黑树的节点添加到红黑树,并且通过左旋或者右旋保证红黑树的平衡。
添加完元素之后判断链表的长度是否大于等于8,大于8 那么会调用treeifyBin()方法。
最后调用addcount()计算数组的总元素个数。
final V putVal(K key, V value, boolean onlyIfAbsent) {
//键和值都不能为空 否则抛出空指针异常
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
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;
//如果tab 为空 那么需要先初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//(n - 1) & hash 计算当前的值应该存放的数组下标
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果计算出的数组位置的node为null 直接用cas进行替换即可
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//MOVED 代表当前节点正在进行数据迁移 那么直接去协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//onlyIfAbsent true表示不能覆盖原有的值 默认是false
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
//如果存在相同的key 并且value不为空 直接返回已存在的值
return fv;
else {
//这段代码是处理存在hash冲突的情况的逻辑
V oldVal = null;
//针对node进行加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
//针对链表的处理
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//遍历链表 如果存在有相同的key 而且允许被覆盖 那么直接覆盖原有的值
//不需要cas来保证线程安全 因为已经加了锁
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;
//
//也就是存在key相同的并且允许被覆盖就覆盖旧的值 不然就根据红黑树的规则添加到树中
//putTreeVal() 解决hash冲突的逻辑和链表一样 但是会涉及到树的左旋和右旋
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");
}
}
if (binCount != 0) {
//查看链表长度是否达到了树化的阈值(默认是8)
// 注意 这里并不是说达到了阈值就会树化
//而是要满足数组长度大于64而且链表长度大于等于阈值两个条件才会树化 否则会先进行扩容来减少链表的长度
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount