onlyit onlyifabsent


大家好,我是阿轩 。
每当我们谈到并发包的时候,脑海中的第一印象就是ConcurentHashMap类,的确,这个类的很多并发设计思想非常值得我们借鉴,阿轩也趁着周末想仔细研究一下,但是每当遇到问题想求助浏览器的时候,发现很多文章都是 ctrl+c ctrl+v,很多细节压根没提,无奈,只能静下心来,自己研究了

onlyit onlyifabsent

文章插图

这篇文章主要是对核心方法 put get 中的细节进行一个详细的剖析,版本是 JDK1.8.0_241,至于 7 和 8 的区别,网上已经有很多文章了,本篇就不再继续啰嗦了,话不多说,我们开始
putput 源码比较长,我们先看上半部分
onlyit onlyifabsent

文章插图

第一个 if 判断集合是否为空,为空进行初始化
onlyit onlyifabsent

文章插图

这里介绍一下 sizeCtl 这个变量
private transient volatile int sizeCtl;他是被 volatile 修饰的,具有线程可见性,当初始化和扩容时是负数,其他时候是正数
刚进入循环的时候,sizeCtl 是正数,所以会走到 ②,这里会使用 CAS 进行无锁赋值,CAS 失败的线程下一个循环来到 ①,让出 CPU 时间片 。
CAS 成功的线程会进行数组的初始化,当我们实例化 ConcurrentHashMap 的时候,如果传入了初始容量大小,那么会对传入的这个参数进行一系列的位运算,最终得到一个 2 的幂次方的值,如果没有传入初始容量,那么 siteCtl 就等于默认值 0,所以如果传入了初始容量,n 就等于计算后的值,没传就等于默认值 DEFAULT_CAPACITY
private static final int DEFAULT_CAPACITY = 16;所以 ConcurrentHashMap 的默认初始化容量是 16,和 HashMap 一样,初始化数组之后会将 sizeCtl 设置为(容量 x0.75) 大小,作为下一次扩容的阈值 。其他自旋等待的线程发现 table!=null 后跳出循环
回到 putVal 方法,此时来到判断 ②,通过 tabAt 方法求出目标索引位的值,(n - 1) & hash等同于hash%n,也就是对容量大小进行取余,计算出目标所在的索引位 i
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}这里通过 Unsafe 的 getObjectVolatile 方法进行取值,getObjectVolatile 具有 valotile 的语义,可以获取 i 索引处的最新值,如果值为 null,新建一个 Node 对象,然后通过 CAS 进行赋值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}如果 CAS 赋值成功,跳出循环,赋值失败,进入下一次循环,来到 ③,判断目标节点的 hash 值是否等于 MOVED,也就是-1,
static final int MOVED = -1;如果等于则帮助扩容,扩容后面再细讲,我们来看最后一个判断
如果目标索引位的值既不为 null,也不在扩容,那么就直接加锁开始判断节点的 key 是否相等 。
这里可能有读者会想,前面赋值的时候使用的是 CAS,那么这里可不可以也使用 CAS 呢
阿轩仔细思考了一下,其实这里是没办法用 CAS 的
onlyit onlyifabsent

文章插图

原因有 3 点