底层结构
Segment 数组,每个 Segment 对象中包含一个 HashEntry 数组,数组元素可以是链表
原理
分段锁(Segment 继承自 ReetrantLock)
构造方法
// initialCapacity、concurrencyLevel 默认 16,loadFactor 默认 0.75
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
- 计算 Segment 数组的长度 ssize:大于等于 concurrencyLevel 的最小 2 的次幂;记录 segmentShift 和 segmentMask (用于插入元素时计算下标)
- 使用 initialCapacity 除以 ssize 计算每个Segment 对象中数组的长度 (最小是 2)
- 创建 Segment 数组,创建第 0 个位置的 Segment 对象
put 方法
- key 和 value 不能为 null,否则抛出空指针异常
- 计算 hash 值 (使用 key.hashCode() 和哈希种子计算)
- 计算下标
即用 hash 值的高位和 length - 1 相与 - 确保下标位置的 Segment 对象已经初始化 (使用了 CAS;以 segments[0] 为原型)
- 调用 Segment 对象的 put 方法,put 的过程与 HashMap 类似 (Segment 算是一个小型的 HashMap),下标计算方式为:hash & length - 1,其中 hash 与计算大数组下标时的相同
- 尝试获取锁,若获取失败则调用 scanAndLockForPut 方法,该方法会循环地调用 tryLock() 方法尝试获取锁,尝试一定次数后调用 lock() 方法阻塞地获取锁,确保最终一定获取到锁
- 尝试获取锁的过程中会遍历链表 (若检测到头节点被其他线程修改则重新遍历并重置尝试次数),若不存在要插入的 key 则创建一个 HashEntry 对象
- 获取到锁后遍历链表,若 key 存在则修改其 value,不存在则先判断是否需要扩容 (扩容条件为 插入后当前 Segment 中元素数量 > 容量 * 加载因子),然后插入新节点,若 scanAndLockForPut 方法返回的节点不为 null,则直接使用,否则 new 一个 HashEntry 对象插入 (头插法)
- put 完成之后释放锁
扩容
- 创建新数组(长度为原来的两倍)
- 迁移数据:链表最后一段新下标相同的元素一次性移过去 (该段元素顺序不变),其他元素一个一个移 (头插法,顺序颠倒)
remove 方法
- 找到所在的 Segment (与 put 方法相同)
- 调用 Segment 对象的 remove 方法 (与 put 方法类似)
- 尝试获取锁,若获取失败则调用 scanAndLock 方法,该方法会循环地调用 tryLock() 方法尝试获取锁,尝试一定次数后调用 lock() 方法阻塞地获取锁,确保最终一定获取到锁
- 尝试获取锁的过程中会遍历链表 (若检测到头节点被其他线程修改则重新遍历并重置尝试次数),寻找要 remove 的 key
- 获取到锁后遍历链表,删除对应节点 (若指定了 value 则必须 key 和 value 都相同才删除)
- remove 完成之后释放锁
size() 方法
循环遍历 segments,统计修改次数(modCount) 和元素个数(count):
- 第一次遍历:记录修改次数初始值
- 第二次遍历:若修改次数没变,则返回统计到的元素个数;若修改次数变了,则记录最新的修改次数,并继续循环,若第三次遍历后修改次数依然变了,则对所有的 Segment 对象加锁并继续循环遍历 (加锁后修改次数就不会变了)
- 若某个 Segment 的 count 溢出了则返回 Integer.MAX_VALUE