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[] tab = table;;) {
//记录当前索引位置的数据
Node 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(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 (Node 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 pred = e;
if ((e = e.next) == null) {
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;
}
}
}
}
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表示的数组的长度,但为什么不是 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[] initTable() { Node [] 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 [] nt = (Node [])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();这里,然后再一次执行U.compareAndSwapInt(this, SIZECTL, sc, -1,然后执行后面的代码,此时如果不再做一次判断,就会初始化多次。。。



