ConcurrentHashMap实现的原理

一、JDK8中的实现原理

在 JDK8 及以上的版本中,ConcurrentHashMap 的底层数据结构依然采用“数组+链表+红黑树”,但是在实现线程安全性方面,抛弃了 JDK7 版本的 Segment分段锁的概念,而是采用了 synchronized + CAS 算法来保证线程安全。在ConcurrentHashMap中,大量使用 Unsafe.compareAndSwapXXX 的方法,这类方法是利用一个CAS算法实现无锁化的修改值操作,可以大大减少使用加锁造成的性能消耗。这个算法的基本思想就是不断比较当前内存中的变量值和你预期变量值是否相等,如果相等,则接受修改的值,否则拒绝你的而操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。

二、JDK7的实现原理

在 JDK7 中,ConcurrentHashMap 使用“分段锁”机制实现线程安全,数据结构可以看成是"Segment数组+HashEntry数组+链表",一个 ConcurrentHashMap 实例中包含若干个 Segment 实例组成的数组,每个 Segment 实例又包含由若干个桶,每个桶中都是由若干个 HashEntry 对象链接起来的链表。

因为Segment 类继承 ReentrantLock 类,所以能充当锁的角色,通过 segment 段将 ConcurrentHashMap 划分为不同的部分,就可以使用不同的锁来控制对哈希表不同部分的修改,从而允许多个写操作并发进行,默认支持 16 个线程执行并发写操作,及任意数量线程的读操作。

ConcurrentHashMap实现的原理_第1张图片

1、ConcurrentHashMap 的 put() 方法添加元素的过程: 

(1)对 key 值进行重哈希,并使用重哈希的结果与 segmentFor() 方法, 计算出元素具体分到哪个 segment 中。插入元素前,先使用 lock() 对该 segment 加锁,之后再使用头插法插入元素。如果其他线程经过计算也是放在这个 segment 下,则需要先获取锁,如果计算得出放在其他的 segment,则正常执行,不会影响效率,以此实现线程安全,这样就能够保证只要多个修改操作不发生在同一个 segment  时,它们就可以并发进行。

(2)在将元素插入到 segment 前,会检查本次插入会不会导致 segment 中元素的数量超过阈值,如果会,那么就先对 segment 进行扩容和重哈希操作,然后再进行插入。而重哈希操作,实际上是对 ConcurrentHashMap 的某个 segment 的重哈希,因此 ConcurrentHashMap 的每个 segment 段所包含的桶位也就不尽相同。

如果元素的 hash 值与原数组长度进行位与运算,得到的结果为0,那么元素在新桶的序号就是和原桶的序号是相等的;否则元素在新桶的序号就是原桶的序号加上原数组的长度

2、ConcurrentHashMap 读操作为什么不需要加锁? 

(1)在 HashEntry 类中,key,hash 和 next 域都被声明为 final 的,value 域被 volatile 所修饰,因此 HashEntry 对象几乎是不可变的,通过 HashEntry 对象的不变性来降低读操作对加锁的需求

next 域被声明为 final,意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改 next 引用值,因此所有的节点的修改只能从头部开始。但是对于 remove 操作,需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。

(2)用 volatile 变量协调读写线程间的内存可见性;

(3)若读时发生指令重排序现象(也就是读到 value 域的值为 null 的时候),则加锁重读;

三、JDK7和JDK8的对比

(1)数据结构:JDK7 的数据结构是 Segment数组 + HashEntry数组 + 链表,JDK8 的数据结构是 HashEntry数组 + 链表 + 红黑树,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率

(2)锁的实现:JDK7的锁是segment,是基于ReentronLock实现的,包含多个HashEntry;而JDK8 降低了锁的粒度,采用 table 数组元素作为锁,从而实现对每行数据进行加锁,进一步减少并发冲突的概率,并使用 synchronized 来代替 ReentrantLock,因为在低粒度的加锁方式中,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中ReentrantLock 可以通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。

(3)统计集合中元素个数 size 的方式:JDK7 是先尝试 2次通过不锁住 segment 的方式来统计各个 segment 大小,如果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有Segment的大小;在 JDK8 中,对于size的计算,在扩容和 addCount() 方法中就已经有处理了,等到调用 size() 时直接返回元素的个数

 转载链接:Java集合篇:HashMap 与 ConcurrentHashMap 原理总结_张维鹏的博客-CSDN博客

你可能感兴趣的:(java,链表,数据结构)