ConcurrentHashMap
为什么使用CHM?
- HashMap线程不安全:1.7环形链表,1.8修复了但多线程数据可能会有丢失问题
- 默认的Collection.toSynchronizedMap 和HashTable就是简单粗暴全hash表上锁,并发度太低。
- 因此使用CHM,提供保证线程安全的同时也能够提供较高并发度的容器。
如何使用CHM?
和HashMap相同,不再赘述。
为什么CHM并发度高且线程安全?
1.7使用分段锁
1.8取消分段锁,采用CAS+sync。
1.7
- 1.7中,数组就是分段的锁Segment,因此对于每个bin的读写加锁,这样子并发度就是数组的长度了,比简单粗暴直接锁整表要好得多。
segment继承ReentrantLock,同步控制依赖reentrantLock。
具体结构和Hashmap不太类似:
- HashMap中,扩容的是大数组
- CHM中,扩容的是对segment进行扩容。
CHM的数组大小同样是2的倍数,原因也是通过位与操作取代取余加速定位。
- CHM也是头插法。
插入的方法流程如下:
-
定位Segment
-
segment中尝试获取锁,获取不到则进行预创建,并等待锁
- 预创建:
-
获取到锁之后则判断是否存在,并进行头插
1.8
1.8中,取消了segment与ReentrantLock,相对于Hashmap的不同点变少了,只是在:
- 获取、变更数组中的node节点时,会加锁
这样子,和1.8中,对比起来支持同时并发的并发度其实是提高了,原来是不会扩容的segment数组,现在是会扩容的数组。
去掉了segment的锁,那么同步的锁是按照如下的情况申请的:
-
put条件下:如果为空,cas地设置该bin为某值;如果失败了(说明有人捷足先登,先插到这个bin里了),那进行下一步
-
如果CAS失败了,并且该节点的hash为MOVED,那么会尝试帮助转换
-
如果不为空,通过sync,锁该bin
- 这里还进行了一下DCL,sync该bin之后还会再检查一次,该索引位置上的是否是该bin
总结
-
CHM在1.7中采用的是Segment分段锁的机制,但其实每个segment可以近似地看作是不同的map,扩容相对独立,但带来的问题就是在数据量增加的情况下,segment的数量不增加,那么会导致并发能力在数据量增大的情况下不变的尴尬情况。
-
1.8中,CHM采用了CAS+sync的方式进行多线程条件下控制,取消了segment的限制,锁的粒度在HashMap中对应的就是数组bin,那么相对来说,随着CHM大小的增加,bin也会增加,变相地增加并发度。
- 但是,1.8中的扩容会锁全表,这个时候读写线程只能帮助扩容,不能做1.7中读取其他段上数据的擦欧总
-
两个版本的CHM中都有在获取不到锁的情况下预处理或者帮忙处理其他的操作:
- 1.7会预处理节点
- 1.8会在整个数组发生转换的条件下,当前线程会做一个帮助其他线程转换的操作。
-
两个版本的put操作都是加锁的,然而get方法不加锁。
-
1.7通过volatile保证不是脏数据
- node里的val,是volatile
-
1.8 node的val和next使用volatile修饰,读写变量可见
数组采用volatile修饰,保证扩容被读线程感知
-
-
在结构上,CHM在两个版本的异同其实就和hashMap中的异同类似:
-
大致上的逻辑并没有进行大的修改:都是数组+其他存储结构,hash值索引,通过扰动函数优化元素分布,通过设定数组长度为2倍数来使用按位与运算替代取余操作这一加速技巧。
-
但是在更下级的存储结构上做了修改:
- 1.7中的分段锁+链表,在1.8中变成了和hashMap一样的bin数组+链表\红黑树,这可能是考虑到并发度和寻址速度的问题。扩容次数毕竟有限,插入读取的操作会更多一些。
-
\