4.1.1、initTable 初始化数组
我们再来看看源码中的第 3 步 initTable()
方法,如果数组为空就初始化数组,源码如下:
sizeCtl 是一个对象属性,使用了 volatile 关键字修饰保证并发的可见性,默认为 0,当第一次执行 put 操作时,通过Unsafe.compareAndSwapInt()
方法,俗称CAS
,将 sizeCtl
修改为 -1
,有且只有一个线程能够修改成功,接着执行 table 初始化任务。
如果别的线程发现sizeCtl<0
,意味着有另外的线程执行 CAS 操作成功,当前线程通过执行Thread.yield()
让出 CPU 时间片等待 table 初始化完成。
4.1.2、helpTransfer 帮组扩容
我们继续来看看 put 方法中第 5 步helpTransfer()
方法,如果f.hash == -1
成立,说明当前f
是ForwardingNode
节点,意味有其它线程正在扩容,则一起进行扩容操作,源码如下:
这个过程,操作步骤如下:
- 第 1 步,对 table、node 节点、node 节点的 nextTable,进行数据校验;
- 第 2 步,根据数组的 length 得到一个标识符号;
- 第 3 步,进一步校验 nextTab、tab、sizeCtl 值,如果 nextTab 没有被并发修改并且 tab 也没有被并发修改,同时
sizeCtl < 0
,说明还在扩容; - 第 4 步,对 sizeCtl 参数值进行分析判断,如果不满足任何一个判断,将
sizeCtl + 1
, 增加了一个线程帮助其扩容;
4.1.3、addCount 扩容判断
我们再来看看源码中的第 9 步 addCount()
方法,插入完成之后,扩容判断,源码如下:
这个过程,操作步骤如下:
- 第 1 步,利用 CAS 将方法更新 baseCount 的值
- 第 2 步,检查是否需要扩容,默认 check = 1,需要检查;
- 第 3 步,如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;
- 第 4 步,如果不在扩容,将 sizeCtl 更新为负数,并进行扩容处理;
put 的流程基本分析完了,可以从中发现,里面大量的使用了CAS
方法,CAS 表示比较与替换,里面有 3 个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,如果相等,就将新值更新到内存地址里,如果不相等,就继续循环,直到操作成功为止!
虽然使用的了CAS
这种乐观锁方法,但是里面的细节设计的很复杂,阅读比较费神,有兴趣的朋友们可以自己研究一下。
4.2、get 操作
get 方法操作就比较简单了,因为不涉及并发操作,直接查询就可以了,源码如下:
从源码中可以看出,步骤如下:
- 第 1 步,判断数组是否为空,通过 key 定位到数组下标是否为空;
- 第 2 步,判断 node 节点第一个元素是不是要找到,如果是直接返回;
- 第 3 步,如果是红黑树结构,就从红黑树里面查询;
- 第 4 步,如果是链表结构,循环遍历判断;
4.3、reomve 操作
reomve 方法操作和 put 类似,只是方向是反的,源码如下:
replaceNode 方法,源码如下:
从源码中可以看出,步骤如下:
- 第 1 步,循环遍历数组,接着校验参数;
- 第 2 步,判断是否有别的线程正在扩容,如果是一起扩容;
- 第 3 步,用 synchronized 同步锁,保证并发时元素移除安全;
- 第 4 步,因为
check= -1
,所以不会进行扩容操作,利用 CAS 操作修改 baseCount 值;
五、总结
虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent
包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!
在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。
但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于 8 时,会将链表转化成红黑二叉树结构。
在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized
操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。
本文因为是断断续续写出来,如果有理解不对的地方,欢迎各位网友指出!
六、参考
1、JDK1.7&JDK1.8 源码
2、JavaGuide - 容器 - ConcurrentHashMap[1]
3、博客园 -dreamcatcher-cx - ConcurrentHashMap1.7 实现原理及源码分析[2]
4、简书 -占小狼 - 深入浅出 ConcurrentHashMap1.8[3]