ConcurrentHashMap 一、介绍
源码:jdk1.8 ConcurrentHashMap是为了解决HashMap线程不安全而设计的一种哈希表实现, 一种以 键-值(key-indexed) 存储数据的结构 。
使用"扁平化的分段锁"(Flattened Segments)的机制。替代了1.7之前的分段锁(Segments),减少了内存开销,并提供更好的并发性能。
使用了大量的CAS操作来进行并发控制,这种操作在多线程环境下能够有效地实现原子性更新。CAS 的引入使得在进行插入、更新和删除操作时,减少了锁的使用,提高了性能。
当链表长度超过一定阈值时,会将链表转换为红黑树,以提高查找和删除操作的性能。
提供了线程安全的 putIfAbsent() 方法,它可以在键不存在时插入一个键值对。这个方法的原子性操作可以在多线程环境中避免重复插入。
二、存储过程
1.首先会对传入的k的hashCode进行运算,获取到一个值用于后续计算存储位置。
散列算法:由源码可知,拿到k值的hashCode后,先右移16位,在和hashCode进行异或运算(这是为了减少hash冲突的发生率,因为在后续进行计算存储位置的时候,会拿数组长度减一在和hashCode进行与操作,如果不进行操作,只会用到hashCode的低位,而不同值对应的hashCode的低位相同的概率很高,这样运算之后,将hashCode的高16位和低16位都用到了,所以降低了hash冲突的概率。HSAH_BITS是Integer的最大值,这样做的好处是确保了算出来的值一定为正数,因为负数在初始化时有特殊的意义,后续会介绍到)
2.首先会判断当前容器是否初始化,如果未初始化,先对其进行初始化,如果已经初始化完成,则通过数组长度减一和上面计算的hash值进行与操作,算出存储的位置,进行了CAS操作对容器进行填充,然后又判断了当前是否在扩容(MOVED == -1)如果正在扩容会进行帮助扩容,之后会对当前hash桶进行加锁,保证线程安全,在该桶中检查是否存在相同的键,如果不存在,则将新的键值对插入到链表或红黑树中(具体取决于当前桶的情况)。在插入的过程中,如果发现当前桶中的链表长度已经达到了阈值(8),则会触发链表转换为红黑树的操作。这个操作涉及到链表节点的重排序,以及树节点的构建,插入成功后对当前hash桶里的值进行判断,如果超过了8且数组的长度超过64将从链表转换为红黑树。如果链表长度超过8,数组长度还没有超过64,将进行扩容操作。
三、扩容机制
1.触发扩容的有三种场景,第一就是在链表转红黑树的时候进行判断,如果此时数组的长度低于64,则不会转成红黑树,会对数组进行扩容
第二是进行putAll()方法的时候,可能会触发扩容机制,因为putAll传入的参数是map对象,如果这个map的数据比较多,就会进行扩容操作
第三就是添加元素的时候,如果达到阈值,会进行扩容机制
2.首先会进行计算扩容标识戳,这个的作用是用来记录当前数组是从什么长度开始扩容的,如果还有其他线程进来,发现自己的数组长度和这个长度一样,就可以协助其扩容,提高效率,第二个作用就是在第一个线程进来的时候,会对这个标识戳进行右移16位,来保证SIZECTL的值为负数,表示当前正在扩容。右移16位之后在加上当前线程数+1来表示当前有多少个线程在进行扩容
3.开始执行transfer方法,这个方法内首先会进行计算一次迁移多少数据到新线程里去
4.第一个进来的线程会对新数组进行初始化,初始化的长度为原数组长度的2倍,所以对其进行左移1位
5.初始化完成后开始进行迁移数据,每次迁移的数据量都为上面计算的出来的,根据cpu的内核数进行计算的,从老数组的后面开始,每次迁移规定数量的数据,如果有其他线程进来,也会迁移一部分数据,迁移完之后会在原位置留一个标识-1,后续如果有线程来查询,会知道这个数据已经被迁移到新数组里了,一直循环,直到数组里的数据全部迁移完成。
6.每次线程迁移完成后SIZECTL会减一,由于前面提到第一个线程进来的时候会对标识戳右移16位后在加2,这也导致全部线程执行完毕后还多出1,这个1的作用就是让最后一个线程从头执行一遍来检查原数组数据是否全部迁移完毕。
四。初始化
ConcurrentHashMap是懒加载,并不会在创建对象的时候进行初始化,只有第一次添加数据的时候才会进行初始化。
线程进来首先会判断当前对象是否为空,判断是否需要进行初始化,如过需要初始化,进入循环,再次判断sizeCtl的值是否小于0,如果小于0就意味着正在初始化或者在扩容中,(sizectl默认初始值为0,表示对象还未初始化,如果等于-1则表示正在进行初始化,如果小于-1则表示正在扩容,且其低16位表示参与扩容的线程数,如果大于0表示该数组下次扩容的阈值是多少),如果当前线程发现sizeCtl值小于0,此时会释放cpu的占用,如果大于0会通过CAS操作对sizeCtl赋值为-1,之后会在次判断当前对象是否为空(这么做有点像单例模式的DCL,防止其他线程刚好走到这一步的时候,有线程已经对其初始化完成),之后就是创建一个长度为n的数组,之后赋值给成员对象,之后会对数组的长度和数组长度右移2位进行差运算,这样做的目的就是计算下一次扩容的阈值,相当于乘负载因子0.75,之后再将这个值赋给sizectl,结束初始化操作。
五。总结
线程安全性:ConcurrentHashMap 提供了线程安全的操作,多个线程可以在不需要外部同步的情况下进行读写操作。每个段(Segment)都有一个独立的锁,使得多线程可以在不同的段上同时进行读写,减少了锁的竞争。
分段设计:ConcurrentHashMap 采用分段设计,将整个数据结构分成多个段,每个段都是一个独立的哈希表。这种设计减少了锁的竞争范围,提高了并发性能。
CAS 操作:ConcurrentHashMap 使用了 CAS(Compare-And-Swap)等原子性操作来保证线程安全性。这些操作能够在不加锁的情况下进行数据的更新。
读操作并发性:ConcurrentHashMap 支持多个线程同时进行读操作,从而在读多写少的场景下获得良好的性能。
自动扩容:ConcurrentHashMap 会根据负载因子(load factor)来自动扩容,以保证数据的均衡分布和高效的查找性能。
弱一致性迭代器:ConcurrentHashMap 提供的迭代器具有弱一致性保证,可能会看到一些更新,也可能会错过一些更新,取决于迭代器的调用时机。
高并发环境优化:ConcurrentHashMap 在高并发环境下表现优秀,特别适合读多写少的情况,可以显著减少锁竞争,提高性能。
适用场景:ConcurrentHashMap 适用于需要在多线程环境下进行并发访问的场景,如并发缓存、并行计算等。
功能丰富的 API:ConcurrentHashMap 提供了丰富的 API,包括线程安全的插入、删除、查找操作,以及诸如 forEach、reduce 等方法,支持各种并发操作需求。