一、新方法
java 1.8之后,HashMap提供了一些新方法,方便了某些特定场景的操作
- compute
// 当key的值不存在时执行
computeIfAbsent(key,k->{
// 业务代码
return xxxx;
})
// 当key的值存在时执行
computeIfPresent(key,(k,v)->{
// 业务代码
return xxxx;
})
// 根据传入函数计算key的值
compute(key,(k,v)->{
// 业务代码
return xxxx;
})
使用举例:
ConcurrentHashMap<String,Integer> chm = new ConcurrentHashMap<>();
@Test
public void testCompute() throws InterruptedException {
String key = "test";
for(int i=0;i<10;i++){
new Thread(()->{
// 通过compute方法做某个key的value的累加
chm.compute(key,(k,v)->{
if(v==null){
v = 10;
}else {
v += 10;
}
return v;
});
}).start();
}
TimeUnit.SECONDS.sleep(2L);
System.out.println(chm.get(key));
}
- merge
// 合并key相同的值,function两个参数代表旧值、新值
merge(key,(oldVal,newVal)->{
// 业务代码
return xxxx;
})
二、实现原理
1.初始化table数组
我们从put方法入手进行分析。
首先会进入初始化table数组逻辑
public V put(K key, V value) {
return putVal(key, value, false);
}
transient volatile Node<K,V>[] table;
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ...省略...
for (Node<K,V>[] tab = table;;) {
// == 1.初始化tab
if (tab == null || (n = tab.length) == 0){
tab = initTable();
}
// ...省略...
// ## sizeCtl变量有状态机的作用
// -- 1、=-1,tab正在初始化(或正在扩容);
// -- 2、> 0,表示扩容因子
// -- 3、<-1,用来计算扩容时的线程参与数
private transient volatile int sizeCtl;
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// `< 0`表示其它线程正在初始化tab,当前线程尝试让出cpu控制权
// (循环中再次进入,tab可能已经初始化好了)
if ((sc = sizeCtl) < 0)
Thread.yield();
// cas方式修改sizeCtl的值,改成-1表示正在初始化;
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 这里用到了double-check
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 计算扩容因子的值,达到`3/4 size`扩容
sc = n - (n >>> 2);
}
} finally {
// 设置成正数,表示扩容因子
sizeCtl = sc;
}
break;
}
}
return tab;
}
2.普通put
- 通过spread()方法计算出的hash必定是正数
- hash为负数有特殊含义
static final int MOVED = -1; // 迁移
static final int TREEBIN = -2; // 树
// ## hash值计算,一定为正;复数有特殊含义(见上面的属性)
static final int spread(int h) {
// int是32位
// `h ^ (h >>> 16)`
// 正数:高16位与0异或;负数:高16位与1异或 (同0异1)
// 低16位与高16位异或,增加随机性
// 上述结果再 `& HASH_BITS`,也就是 `& 0x7fffffff` = & 0111 1111..(后面全是1)
// 得到的一定是一个正数(最高位0表示正)
return (h ^ (h >>> 16)) & HASH_BITS;
}
- 通过
(n-1)&hash
计算出落点桶
- 如果桶为空,新建Node后cas替换赋值
- 如果桶中有元素,先对头元素加锁,然后根据元素类型分别处理
来看下元素为链表的情况:
遍历链表,如果找到key相等的元素,替换;如果未找到,新建Node尾插链表
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ## key和val均不能为空
if (key == null || value == null) throw new NullPointerException();
// ## hash值计算
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// ...省略初始化逻辑...
// == 1.桶没有节点,cas方式创建
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// ...省略...
// == 2.桶对应的节点有值的情况(又分链、红黑树几种情况)
else {
V oldVal = null;
// ## 对桶指向的Node加锁
// 在前面的逻辑中:i-hash计算的落点桶的索引;f-桶i的Node对象;fh-f对象的hash值
synchronized (f) {
// double-check
if (tabAt(tab, i) == f) {
// -- f对象的hash值大于等于0,表示链
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// -- 在链上找到了key相等的元素,替换value
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;
// -- 控制链的节点移动;最终在链上没找到key相同节点,则尾插节点
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// -- 节点是树的情况
else if (f instanceof TreeBin) {
//...省略...
}
}
}
//...省略...
3.put引发扩容
1、扩容条件
满足条件之一时触发
- 链上元素>=8,元素个数<64
- 元素个数超过扩容因子
扩容前会计算扩容戳,扩容戳的计算与元素个数有关,同时也会与sizeCtl产生联系
由此得出两个结论
- 扩容时,sizeCtl必然为负数;
- 如果sizeCtl-2=rs<<16,表示扩容结束
2、数据迁移
扩容会引发桶数据迁移
- 每个线程负责迁移一定数量的桶
上图中线程A分到桶16-31,进行迁移;
之后线程B分到桶0-15,帮助迁移;
再有线程C进入,试图帮助迁移(helpTransfer方法)——由于桶已经被迁移线程瓜分完了(图中情况),无需帮助。线程C会直接向新tab的桶中put数据
- 如果落点桶为空
将一个Forwarding Node(fwd)放入桶中,fwd的nextTable属性指向新table
- 如果落点桶为树(忽略)
- 如果落点桶为链
写不下了,下一篇继续分析(下篇包含链的迁移原理
、元素计数
、get方法
等)