1.8版本的ConcurrentHashMap分析
先看看文档
Retrievals reflect the results of the most recently completed update operations holding upon their onset. (More formally, an update operation for a given key bears a happens-before relation with any (non-null) retrieval for that key reporting the updated value.)
happens-before 是重点。
内部的注释
1 | * We use the top (sign) bit of Node hash fields for control |
Node 节点的整形的 Hash 属性,二进制的高几位用来作为状态使用。hash 为负的 Node 表示 Node 需要被特殊处理。负数的状态:
1 | static final int MOVED = -1; // hash for forwarding nodes |
1 | * Maintaining API and serialization compatibility with previous |
构造器里的 concurrencyLevel 和 loadFactor 没啥用了,后者只是用来初始化最初的 table。
还是看代码吧
构造器
1 | public ConcurrentHashMap(int initialCapacity, |
- tableSizeFor 是取大于或等于 size 的最小的 2 的乘方(此处不完全确定),但是返回结果不会大于 MAXIMUM_CAPACITY。算法见《Hackers Delight - 算法心得:高效算法的奥秘》 3.2 节,回头有时间去看看真本书。
- HashMap 的装载因子解释 What is the significance of load factor in HashMap?
- concurrencyLevel 没啥用了,见上面的注释。
- loadFactor 用来计算初始化 table 大小,元素容量除以 loadFactor 可以得出 rehash 的阀值,首次初始化的 table 大小大于这个阀值,以后 loadFactor 都用不到了。
1 | public ConcurrentHashMap(int initialCapacity) { |
- 初始化的 table 的容量是大于或等于 (ic + ic / 2 + 1) 的最小的 2 的乘方。
put
spread
1 | static final int spread(int h) { |
spread 的用处:Java HashMap工作原理及实现
initTable
1 | private final Node<K,V>[] initTable() { |
- sizeCtl 属性:负数表示 table 正在初始化或者扩容,-1 表示正在初始化,- (1 + 扩容线程数) 表示正在扩容。当 table 为 null 时,sizeCtl 为初始化时创立的 table 长度。初始化以后,sizeCtl 的值为 next element count value upon which to resize the table(TODO)。此处 element count 看起来是 k-v 数。
- initTable 方法无锁实现 table 初始化。初始化 table 的操作放在临界区内,临界区由 CAS 设置 sizeCtl 保证。临界区内的逻辑会设置 table 为新的非空的数组,并且 table 是 volatile 的,其他在临界区外的线程会最终全部退出循环,从循环下面的 return 返回。
- 未进入临界区的线程不会疯狂循环消耗 CPU,通过调用 Thread.yield() 释放 CPU 时间。
tabAt
1 | static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { |
ABASE 是 Node [ ] 类型的在内存中的基础偏移,通过调用 unsafe.arrayBaseOffset 获得。ASHIFT 是 Node[] 间隔元素之间内存偏移数的二进制位数,计算方法如下:
1 | int scale = unsafe.arrayIndexScale(arrayClass); |
计算数组中一个元素的内存偏移直接理解应该是:基础偏移 + ( 下标索引 * 元素偏移量 )。但是因为数组元素的内存偏移都是 2 的指数,所以这个计算可以等价于:基础偏移 + 下标索引向左偏移元素偏移量的位数,等价于执行乘法。这里是一个优化,位运算的效率比做乘法要好。这里能这么做也是因为元素偏移量是 2 的指数,一个数乘以一个 2 的指数,相当于向左偏移乘数的位数。测试代码
putVal
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
addCount
1 | private final void addCount(long x, int check) { |
- ThreadLocalRandom 干啥的,sun.misc.Contended 是干啥的,cache line 又是啥?what-is-false-sharing , Oracle JDK8在JVM上的改进有哪些意义? - RednaxelaFX的回答 - 知乎
- addCount 和 fullAddCount 用到了 ThreadLocalRandom,待分析
- ThreadLocalRandom.getProbe() 获取的是啥?ThreadLocalRandom.advanceProbe(h) 又是啥?JAVA THREADLOCALRANDOM EXPLAINED , Xorshift
- ThreadLocalRandom.getProbe() & m 的数学含义是啥? 用一个数去和另一个数求或运算的意义是啥?
resizeStamp
1 | /** |
解析:https://stackoverflow.com/questions/47175835/how-does-concurrenthashmap-resizestamp-method-work
n 为 hash 数组的长度,numberOfLeadingZeros 取从左开始的二进制位中为 0 的个数。1 << (RESIZE_STAMP_BITS - 1) 是将 1 左移动 15 位,与前面的数做或运算,刚好将前述值的左起第 17 位设置为 1。
这个或运算的意义是,如果将 resizeStamp 方法的结果左移 RESIZE_STAMP_BITS 位,也就是 16 位,那么最高位刚好是 1,也就是负数。正好用于 sizeCtl,sizeCtl 为负时表示正在进行扩容,并且低 16 位表示当前正在进行扩容的线程数,如果低 16 位为 2,就代表当前一个线程在执行扩容。这里是 2 是因为最初初始化的时候设置为 2,并且注解中也是这么定义,原因待分析 TODO:
1 | // some code |
资料:https://www.cnblogs.com/grey-wolf/p/13069173.html#_label4
fullAddCount
在 CAS 递增 baseCount 时,遇到竞争时,使用 fullAddCount 去在多个统计变量上去做递增,这个多个统计变量就是 counterCells,这个数组的每一项是一个 CounterCell,是一个统计。
1 | // x: 递增数量 |
- cellsBusy:1 表示 CounterCells 正在创建 ,0 表示其他状态。
- baseCount:用于统计 Map 中的 k-v 数,baseCount 用于在没有出现竞争的情况下统计。
- counterCells:数一个数组,每个数组项是一个 int,在 baseCount 上的递增出现竞争时会去取 counterCells 中的一个项进行递增,最终的 Map 中的 k-v 数总和是 baseCount 和 counterCells 所有计数之和,见下面的 subCount 方法。
- 三个地方都回去 CAS set cellsBusy,从 0 改成 1,并发下只有一个线程能进入临界区代码。临界区代码用 try finally 去保证 cellsBusy 最终一定会被设置回 0,相当于解锁。
- 如果 A 线程运行到「A」,另 B 线程运行到「B1」,如果 A 线程被挂起(比如 CPU 切换了执行线程),然而 B 线程继续执行,一直执行到了「B2」,这个时候 A 线程继续执行,如果没有 counterCells == as 判断,实惠重复创建 counterCells 的。这个是需要 counterCells 判断的理由。这个和并发下的单例设计模式一样,在进入锁以后需要重新判空一次。
- 这里的计数是使用了 counterCells,从源码的注释看,这个实现思路是和 java.util.concurrent.atomic.LongAdder 一致的,可以去研究下。
- fullAddCount 方法的实现思路几乎和 LongAdder 一致。
sumCount
1 | final long sumCount() { |
counterCells 和 baseCount 见上面的解释。
get
1 | public V get(Object key) { |
transfer
… todo
transferIndex
1 | /** |
资料
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html
1.8版本的ConcurrentHashMap分析
http://footmanff.com/2018/03/13/2018-03-13-ConcurrentHashMap-1/