这里需要说明一下,本人才疏学浅,写下这些东西,完全是暂时记下自己的一些理解,无法保证理解的正确性,想要学习ConcurrentHashMap的还是不要看.
慎
注意点
-
为什么使用
ConcurrentHashMap
,是因为需要在一个线程安全的映射集合类,那为什么不使用HashTable呢?其实就是我在HashTable的博客中说到的,HashTable的锁粒度实在太大,虽然保证了线程安全,但是并发性能无法保证,那么ConcurrentHashMap
是如何实现线程安全的呢,是通过分段锁来实现 -
因为1.7和1.8在HashMap和ConcurrentHashMap的实现上还是不同的,这里需要分开说:
-
1.7
- 首先,ConcurrentHashMap的最底层是一个segment数组,segment是一个 内部类,它继承了ReentrantLock这个类,这个类是可重入锁,重入锁允许同一个线程可以重复进入,也就是这个锁,实现了ConcurrentHashMap中的分段锁,每一个segment就是一段,其内部实现其实很像一个hashMap.
- ConcurrentHashMap初始化的参数:
- segment数组的默认大小是16,也就是并行度,最多允许多少个线程同时访问,segment数组初始化以后就不能扩容了,但是每个segment内部的数组是可以扩容的.
- segment分为了16个,而每一个segment的初始大小时2,阀值就是2*0.75=1.5(这个阀值代表每一个segment填入第二个键值对时就需要扩容了),初始化的时候只会初始化第一个segment,也就是说segments[0],其余的segment还都是null,等使用的时候才进行初始化.
- 上面也说到了,每一个segment内部是一个HashMap,而我们无论是查询,添加,删除,很多方面都是类似HashMap,不同的是需要先选择使用哪一个segment,然后在segment内部再次进行hash定位,找到对应的位置,有意思的是,
ConcurrentHashMap
维护了两个变量segmentShift
和segmentMask
,具体是怎么得到的不说了,这两个变量主要用于计算机快速定位segment数组中的位置. - segment初始化的问题,前面说到过,实例化map时只会初始化第一个segment,其他的segment只会在第一次使用时初始化,而如果有多个线程同时触发一个segment的初始化怎么办呢?这里用到的是一个CAS,熟悉并发的肯定对CAS很了解,主要就是在循环中不断检查和初始化,如果本线程初始化成功或者其他线程初始化成功就退出循环.
- 说一下加锁的问题,在put方法内部,定位到一个segment后,就会为这个segment添加独占锁,这个添加的过程是用tryLock()循环进行的,循环有两个出口,一个是成功初始化,另一个就是循环次数超过了一个阀值.然后使用lock()加锁阻塞线程直到加锁成功.
- 然后说一下扩容的问题,虽然前面说了一个segment内部就是一个HashMap,但是还是有很多不同的,在扩容方面因为要考虑并发的情况.
- 并发问题
- 并发问题主要是讨论使用get操作时,同时还有put或remove操作和扩容,因为put和remove都是有独占锁的,但是get是无锁的.
- get时使用了put操作:
- 先get操作,如果是先进行的get操作,这是get正在链表上寻找值,而新插入的值是插到链表表头,所以没有影响,
- 先put操作,需要保证插入的值也被扫描到,具体有
unsafe.putOrderedObject
方法,这个方法保证可见性
- get时发生扩容
- 因为底层数组table是volatile修饰的,所以如果是先put导致扩容,有volatile保证可见性,如果get先行,就是在原来的数组上扫描.
-
1.8
- 1.8我不会说太多,不是因为和1.7差不多,而恰恰相反,1.8和1.7差别是很大的,一开始我很难理解,到现在其实还是不太理解,只能简单说说,等多看几遍,再补充
- 1.8中似乎是没有分段锁的概念了,至少在我看来是这样的,虽然还是有segment这个内部类,但是其内部只有一个负载因子的属性,其次就是整个
ConcurrentHashMap
使用的是一个数组,数组元素是一个链表或者红黑树,和HashMap是很相似的,但就是在这样的底层数据结构上,还是实现了线程安全. - 1.8中维护了一个属性—sizeCtl,这是一个状态属性,它的值代表了底层数组的在多线程情况下的状态,这个值的改变都是通过CAS操作来完成的,
- 1.8中的锁粒度更小了,从putVal方法来看,先通过CAS尝试插入值,如果失败就代表有并发情况,然后是通过同步锁,锁是通过
synchronized
来实现的,但锁住的只是数组中的链表头结点对象,这样锁粒度很小.是很好的一个实现. - 和HashMap中不同的是,ConcurrentHashMap不允许key和value为null.会抛出空指针异常.
- 扩容问题:因为底层只有一个数组,所以扩容就只是一个任务,但是这个任务不会由一个线程来完成,因为任务太大,所以会为每个线程分配一部分数组的复制迁移工作,至于的具体的,还没有很看懂.