ConcurrentHashMap的初步使用及场景
CHM的使用
ConcurrentHashMap是J.U.C包里面提供的一个线程安全并且高效的HashMap,所以ConcurrentHashMap在并发编程的场景中使用的频率比较高,那么这一节课我们就从ConcurrentHashMap的使用上以及源码层面来分析ConcurrentHashMap到底是如何实现安全性的
api使用
ConcurrentHashMap是Map的派生类,所以api基本和Hashmap是类似,主要就是put、get这些方法,接下来基于ConcurrentHashMap的put和get这两个方法作为切入点来分析ConcurrentHashMap的源码实现
ConcurrentHashMap的源码分析
先要做一个说明,这节课分析的ConcurrentHashMap是基于Jdk1.8的版本。
JDK1.7和Jdk1.8版本的变化
ConcurrentHashMap和HashMap的实现原理是差不多的,但是因为ConcurrentHashMap需要支持并发操作,所以在实现上要比hashmap稍微复杂一些。
在JDK1.7的实现上,ConrruentHashMap由一个个Segment组成,简单来说,ConcurrentHashMap是一个Segment数组,它通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全。整个结构图如下
当每个操作分布在不同的segment上的时候,默认情况下,理论上可以同时支持16个线程的并发写入。
相比于1.7版本,它做了两个改进
- 取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率
- 将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的结构。为什么要引入红黑树呢?在正常情况下,key hash之后如果能够很均匀的分散在数组中,那么table数组中的每个队列的长度主要为0或者1.但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为O(n); 因此对于队列长度超过8的列表,JDK1.8采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN),可以提升查找的性能;
这个结构和JDK1.8版本中的Hashmap的实现结构基本一致,但是为了保证线程安全性,ConcurrentHashMap的实现会稍微复杂一下。接下来我们从源码层面来了解一下它的原理.
我们基于put和get方法来分析它的实现即可
put方法第一阶段
假如在上面这段代码中存在两个线程,在不加锁的情况下:线程A成功执行casTabAt操作后,随后的线程B可以通过tabAt方法立刻看到table[i]的改变。原因如下:线程A的casTabAt操作,具有volatile读写相同的内存语义,根据volatile的happens-before规则:线程A的casTabAt操作,一定对线程B的tabAt操作可见
initTable
数组初始化方法,这个方法比较简单,就是初始化一个合适大小的数组
sizeCtl这个要单独说一下,如果没搞懂这个属性的意义,可能会被搞晕
这个标志是在Node数组初始化或者扩容的时候的一个控制位标识,负数代表正在进行初始化或者扩容操作
-1 代表正在初始化
-N 代表有N-1有二个线程正在进行扩容操作,这里不是简单的理解成n个线程,sizeCtl就是-N,这块后续在讲扩容的时候