ConcurrentHashMap put方法分析(基于jdk1.8)
源代码
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;
//死循环,声明一个tab,其值是当前concurrentHashMap
for (Node<K,V>[] tab = table;;) {
//记录当前索引位置的数据
Node<K,V> f;
// n数组长度 i数据要存储的索引位置
int n, i, fh;
if (tab == null || (n = tab.length) == 0){
tab = initTable();
}
// (n - 1) & hash作为索引去找数据并赋值给f
// tabAt(数组,i) 获取数组中索引i位置的数据
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// new 一个node,然后通过cas操作,将当前node放入到tab的i位置。
// casTabAt(数组,1,2,3) 用CAS的方式,将1位置上的数据从2修改成3
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))){
break;
}
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 下面这部分代码是发生hash冲突时候,怎么进行put
V oldVal = null;
// 锁住当前桶的位置
synchronized (f) {
//拿到i位置的数据,判断和锁住的是不是同一个
if (tabAt(tab, i) == f) {
//当前桶下是链表,或者是空
if (fh >= 0) {
//计数器
binCount = 1;
//因为链表只能一个一个往下找,所以需要用到for循环
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 判断是否hash冲突,并且key值是完全一样,如果是一样就不是添加,而是修改操作
if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {
oldVal = e.val;
//如果不是,则覆盖数据,如果是true,什么都不做
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;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
如何计算索引位置,尽可能的避免hash冲突
上面的代码中有很重要的一步:根据索引找出tab数组中的数据,即
tabAt(tab, i = (n - 1) & hash)
要解释这行代码,需要先了解位运算符的细节,以及索引的处理过程 (n - 1) & hash)
(n - 1) & hash
n表示的数组的长度,但为什么不是 n & hash
,而是(n-1)&hash
呢?
我们假设n的值是默认值,16。那么16的二进制是
00000000 00000000 00000000 00010000
假设hash值是
00000110 00011000 00111010 00001110
&,即:
00000000 00000000 00000000 00010000
&
00000110 00011000 00111010 00001110
=
00000000 00000000 00000000 00000000
由于0&0 0&1最终结果都是0,可见最终计算出来的值不能尽可能地分散开,如果是减1,那么结果就会有所不同
00000000 00000000 00000000 00001111
&
00000110 00011000 00111010 00001110
=
00000000 00000000 00000000 00001110
按照将计算结果尽可能分散开,避免hash冲突的原则,下面的spread()
函数也是为了达到这个目的。
static final int spread(int hash) {
return (hash^ (hash>>> 16)) & HASH_BITS;
}
这里用到了无符号右移,为什么要有这样一步操作呢?
我们之前讲的(n-1)&hash
是hash值与数组长度进行&运算,但是这里出现一个问题,就是如果数组长度比较小,比如说只有15,那么高位就一直不会参与&运算,而(hash^ (hash>>> 16)) & HASH_BITS
恰恰就是为了让高位也参与进来进行运算
以00000110 00011000 00111010 00001110
举例
00000110 00011000 00111010 00001110
^
00000110 00011000 00111010 00001110
即:
00000000 00000000 00000110 00011000
^
00000110 00011000 00111010 00001110
=
00000110 00011000 00111010 00001110
这样的话,又一次将值分散
初始化数组initTable()
sizeCtl 用来控制数组初始化的变量
1、sizeCtl < -1 表示正在扩容
2、sizeCtl = -1 表示map的数组正在初始化
3、sizeCtl = 0 还没有初始化
4、sizeCtl > 0 已经初始化,表示阈值
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//判断数组是否初始化
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//compareAndSwapInt在底层肯定有锁的,只是在java层面看起来没有锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//进来了说明没有还初始化,代表开始初始化!!!
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//new 出Node节点,然后复制
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
(tab = table) == null || tab.length == 0
要执行俩次,是为了保证不会初始化多次:
假设线程A已经在执行初始化,当他执行到sc = n - (n >>> 2);
时候,sc值改变了,那么线程B,此时可能在Thread.yield();
阻塞着,在线程A初始化完成后,线程B不再阻塞而是向下执行,如果不再做一次(tab = table) == null || tab.length == 0
,那么new Node的操作会初始化多次,这显然和预期不匹配
treeifyBin 转红黑树的操作
put方法里面的finally代码块中存在一个函数 treeifyBin,这块代码是进行扩容和红黑树转换用的
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//获取数组长度,如果小于最小树化容量,那么就尝试扩容
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));
}
}
}
}
}