参考: sizeCtl : https://blog.csdn.net/Unknownfuture/article/details/105350537
- https://www.jianshu.com/p/c0642afe03e0
- https://www.cnblogs.com/renqiqiang/p/9451301.html
- https://www.jianshu.com/p/c0642afe03e0
- 深入理解Java——ConcurrentHashMap源码的分析(JDK1.8) https://cloud.tencent.com/developer/article/1417971
- UnSafe : https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
并发
- UnSafe类 保证并发线程安全操作
- synchronized
数据结构:
table 数组
加了volatile, 保证线程可见性;
初始化table数组 , 如何保证并发场景下仍能初始化一个数组?
- 通过CAS保证
先看一个重要字段 sizeCtl , 数组初始化的真实长度,即table数组申请内存的长度;
- 默认0 ,
- 构建Map时,构造方法里会初始化该值
- CAS修改该值
initTable()方法
/**
* The default initial table capacity. Must be a power of 2
* (i.e., at least 1) and at most MAXIMUM_CAPACITY.
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* Initializes table, using the size recorded in sizeCtl.
*/
private final ConcurrentHashMap.Node<K,V>[] initTable() {
ConcurrentHashMap.Node<K,V>[] tab;
int sc;
//自旋,保证一定会初始化table成功;
while ((tab = table) == null || tab.length == 0) {
/**
* sizeCtl取值
* sizeCtl > 0, 会在ConcurrectHashMap的构造方法中赋值, 即由指定的初始容量 通过tableSizeFor()方法 计算得来,
* sizeCtl = 0, 未指定初始容量
* sizeCtl = -1, 表示table数组正在进行初始化; 即下面 U.compareAndSwapInt(this, SIZECTL, sc, -1) 通过CAS将sizeCtl变量置位-1;
* sizeCtl = -N, 取-N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容。
*/
if ((sc = sizeCtl) < 0)
//使当前线程由执行状态,变成为就绪状态,让出cpu时间片,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//CAS , 如果sizeCtl >= 0, 说明未初始化和准备初始化,通过CAS置位-1, 保证并发;
try {
if ((tab = table) == null || tab.length == 0) {
// 如果sc == 0, 则使用默认初始容量 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n]; //初始化Node数组
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
//还原sizeCtl值, 因为上面通过CAS将sizeCtl改为-1了,这里进行还原
sizeCtl = sc;
}
break;
}
}
return tab;
}
下面主要看下,当线程 t1 进入initTable(); 的时候,这时线程 t2 也符合tab == null添加下则进入 initTable();方法,这个时候如何保证 t1,t2 扩容时候的线程安全;
保证方式:其实首次对map进行扩容的时候,即初始化table变量的时候,只需要保证第一个线程进入时进行初始化,其他线程无法执行即可。
这时通过CAS保证update只有一个线程成功即可。
下面看看 initTable() 这个方法的实现方案:
if ((sc = sizeCtl) < 0) // 初始化时为0 当为负数的时候线程 进入yield()方法
Thread.yield(); // yield()方法会通知线程调度器放弃对处理器的占用,当前线程放弃执行权
如果发生Hash冲突,如何保证线程安全的写入?
通过 synchronized 关键字,加重量级锁;
Node节点:
被Volatile修饰,保证线程间可见。
Q: Volatile如何保证可见性?
Volatile的实现原理,使用到了内存屏障(Memory Barrier); 参考: https://www.jianshu.com/p/d3fda02d4cae
内存屏障的作用或者效果:
- 保证指令不会被重排序;
- 影响某些数据的内存可见性;
如果一个变量是volatile修饰的,则 JMM(Java Memory Model)Java内存模型会保证
读这个字段变量前, 插入一条Read-Barrier 指令, 该指令会强制刷新缓存cache,即 各个线程本地缓存强制刷新到Jvm 主内存,
写这个变量后, 插入一条 Write-Barrier 指令, 该指令会强制把本地线程的缓存数据刷新到JMM主内存;
不同线程在操作一个变量对象时,会从主内存里拷贝一份到自己的本地缓存里, 也叫线程本地变量,对线程本地变量的数据进行处理, 并不会立即刷新到主内存;volatile关键字 使用的内存屏障,通过Read-Barrier 和Write-Barrier 指令, 保证了主内存数据的一致性;实现了可见性;
每个线程都有自己的本地内存Local Memory(只是一个抽象概念,物理上不存在),存储了该线程的共享变量副本,
所以,线程A和线程B之前需要通信的话,必须经过一下两个步骤:
1、线程A把本地内存中更新过的共享变量刷新到主内存中。
2、线程B到主内存中读取线程A之前更新过的共享变量。
构造方法
可见, ConcurrentHashMap与HashMap一样, 都是懒加载, 使用的时候才会创建hash桶;
put()方法
put()方法流程:
- 检查key、value是否空, 为空则抛异常;
- 通过key, 计算hash值;
- 自旋
- 增加总数
在第3步自旋时,又做了如下:
- 判断表是否空, 如果则初始化,自旋重试;
- CAS判断hash相应位置是否无元素, 如果无,则新插入, 否则,自旋重试;
- 相应hash位有元素, 判断其hash值是否是-1, 如果是, 则说明其他线程正在扩容, 则一起扩容,然后自旋重试;
- 对一个元素加锁 synchronized(f); put数值