ConcurrentHashMap源码阅读
线程安全,在类注释中就直接表面这个是为了替代HashTale来实现的,并且兼容HashTable的操作方法。
1.7版本
内部维护了一个Segment的内部类,通过继承ReentrantLock来实现,为了简化一些锁操作,避免单独的构造。Segment维护了一个entry列表来保持一致性,所以可以在读的时候无锁,然后再segment的内部使用hashentry数组来维护数据。
初始化
大体步骤如下:
- 参数校验
- 计算掩码,偏移量操作,默认构造掩码是15,偏移量28
- 初始化,segment的容量是
sszie,是通过循环左移1,直到找到一个小于concurrentLevel附近的数,Segment[0]的HashEntry[]大小是cap,通过initialCapacity / ssize计算得到c,然后移循环左移找到数据,其是又是一个小于c附近的2的幂次值Segment[]的大小是sszie- 将s0放到ss中是通过
unsafe类的putOrderedObject操作实现的
初始化segment[0],默认为2,负载因子是0.75,扩容的阈值就是1.5,只有插入第二个数值的时候才会扩容。
put操作
1 | /** |
在此版本中的hash算法,当然也就要传入调用hashcode方法后的值,也是遵循着冲突容易发生在低位,所以用移位,异或,求和操作得到一个值,最后再与自己右移16的数进行异或操作得到最终的哈希值。
对于存储在哪个segment位置的计算,在使用默认的构造情况下,相当于将hash值右移28位后再与掩码15进行与运算。
在发现插入位的segment为null的时候,调用ensureSegment操作进行初始化,对于这个初始化,流程大体如下:
- 从0中获取初始化长度作为cap
- 从0中获取负载因子
- 计算扩容阈值
- 创建一个cap容量的
HashEntry数组
这个操作类似构造器初始化的时候创建segment[0]的数据,当然不同的是,在这个操作中,会利用unsafe类中的自旋操检测是否还是null,然后再使用unsafe类的cas操作。
在最后插入值的时候,首先是使用tryLock获取锁,未活得则调用scanAndLockForPut方法自旋的获取锁,超过Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1的时候就使用lock()强制获取锁。
使用(tab.length - 1) & hash计算出插入的位置,对这个HashEntry链表进行遍历:
- key存在,直接替换
- 不存在,接着遍历
- 容量不够,扩容,也就是移动到了扩容位
- 移动到最后,也就是key不存在,也扩容结束,就使用头插法插入数据
扩容rehash
从注释和实际代码中,我们可以看到,扩容是直接扩容为原来的2倍,老数组里的数据移动到新的数组中的时候,要么不变,要么是index+oldsize,最后用头插法插入新节点。
1.8版本
当然和同版本的HashMap一样,修改成了链表变红黑树的实现,对于位置从原来的segment修改成了node数组。对于构造器,只是申明的大小,只有在插入数据的时候才会去开辟空间。
在操作前面有tabAt,casTabAt和setTabAt三个前置操作,进行数据可见性的操作,setTabAt操作始终锁定在锁定区域内进行,因此只要求发布顺序。
对于hash函数,其与HashMap不同,是在右移16,减少冲突后,然后在与本身进行异或操作,这个适合会出现负数请求,所以再与0x7fffffff进行并操作后,就能获取正数。
1 | /** |
get操作
在get操作的适合,使用tabAt来进行可见性操作。其原理比较简单,根据哈希值和tabat找到node节点,头简单就是就返回,是红黑树就用find函数查找,是链表就遍历查找。
put操作
当添加到table表空间点的时候:
- 为null,也就刚申明好,这个时候使用
initTable调用进行空间的开辟 - 桶内为空,也就是刚初始化好,进行的第二次循环,这个时候直接使用cas操作放入数据
- 当当前位置的
hashcode == MOVED == -1就进行扩容,和之前的tabat操作有关 - 代码顺序,在tabat操作的时候已经找到了需要插入node节点的位置,在下一次循环的时候就可以进行真实的数据插入操作;
- 直接使用synchronized锁住当前节点
- 再次使用tabat进行同步,fh值是通过hash得到的,为正数的时候说明还是最初的列表,然后在次节点链表中进行遍历,同时进行bincount的自增操作,key相同则替换数据,移动到末尾则插入数据
- 如果是红黑树,则使用红黑树的插入操作。
- 最后检查bincount的值,大于树化阈值则进行书化操作
remove操作
通过浏览remove的代码,可以看到大体逻辑和put操作类似,但是可以发现,在红黑树删除的代码里面,有这么一行setTabAt(tab, i, untreeify(t.first));,也就是说当冲突减小到阈值的时候,又会变为链表。
Author: moyu-x
Link: http://moyu-x.com/2020/08/05/202008/concurrenthashmap/
License: 知识共享署名-非商业性使用 4.0 国际许可协议