【面试题】ConcurrentHashMap实现线程安全的底层原理到底是什么?
JDK1.7以及之前的版本,多个数组,分段加锁,一个数组一个锁
JDK1.8及以后的版本,优化细粒度,整合为一个数组,对数组中每个元素进行CAS,如果CAS失败了说明当前有人了,此时synchronized对数组元素加锁,使用链表+红黑树进行处理,对数组每个元素加锁。
目前较多情况下,多线程要同时读写一个HashMap
原始用法
HashMap map = new HashMap();
synchronized(map){
map.put("xxxx","yyyy");
}
如果线程1put数组[5],线程2put数组[21],其实这两个操作互不影响,除非同时对一个元素执行put操作,才需要多线程进行同步。
改用ConcurrentHashMap(JDK并发包中默认实现了线程安全性的ConcurrentHashMap)
ConcurrentHashMap map = new ConcurrentHashMap();
map.put("xxxx","yyyy");
在JDK1.7以及之前的版本里,分段[数组1],[数组2],[数组3],每个数组对应一个锁,分段加锁
JDK1.8之后进行了优化和改进,利用CAS策略,可以保证并发安全性。(只有对同一个元素进行操作才会用,而多线程对数组中不同的元素执行put时,其实互不影响)
同一时间,只有一个线程能成功执行这个CAS,就是说刚开始先获取一下数组[5]这个位置的值,如果为null,然后执行CAS,compareAndSet将值设置进去,同时其他的线程执行CAS都会失败
通过对数组每个元素执行CAS的策略,如果是很多线程对数组里不同的元素执行put,互不影响。如果其他线程失败了,表示数组[5]这个位置有线程完成了put操作。这个时候需要再这个位置基于链表+红黑树进行处理,synchronized(数组[5])加锁,基于链表或是红黑树在这个位置将自己的数据put进去
如果你是对数组里同一个位置的元素进行操作,才会加锁串行化处理;如果是对数组不同位置的元素进行操作,此时大家可以并发执行
【评论区】
看一下put操作的流程(初始化,上锁,扩容,树化,synchronized)过程
从源码上看,PUT操作只有在NULL时才会CAS
1.ConcurrentHashMap进行put操作的时候,元素为null,则进行cas,不为null,则进行synchronized同步,不为null的时候,为什么也不进行cas呢?(这块的解答看不懂)
因为此时数组得位置放的是链表或者红黑树得引用.而不是具体得值
当数组得位置存放得是链表或者红黑树得引用得时候,此时已经发生了一次哈希冲突了,讲道理,哈希冲突得概率有,但不是很高,链表里面得数据进行添加得时候,运用cas操作是比较麻烦得,因为在多线程情况下往链表里面插数据是会报错得,这个时候只能采用sync锁来进行了.cas是比较后set,那对链表来讲,这个操作是实现不了的,链表在内存中不连续,节点的增加记录下上一个的位置就行,cas操作跟谁比较,怎么比较,hashmap的entry是个单向链表,cas操作中链表的指针如果不为null,那就得到另一个地址在进行比较,在这个过程中,链表长度突破8,对于后续的转化红黑树的操作影响也是非常巨大的.总的来讲,就是这个过程中进行cas的消耗,以及对编码的复杂度,都没有sync来的方便快捷,哈希冲突也是个小概率事件,对数组的不同位置加锁或者cas操作,已经完全够用了
元素不为null,当然不能走CAS,因为数组的元素实际上是一个链表或者红黑树的节点的引用,你要怎么对该节点直接CAS呢?只能先用synchronized(数组元素)锁住后,再对链表或者红黑树做增删操作
上一篇: vue的双向绑定原理(数据驱动)
下一篇: 有趣的css