如果对文章中提到的与 HashMap 相关的部分有任何疑问, 请移步 HashMap源码详解
简介
- 底层是一个
Segment[]
数组, 每个Segment对象
内部又有一个Entry[ ]
数组, 一个Entry[]
数组就相当于一个HashMap Entry[ ]
采用拉链法解决冲突, 但是没有红黑树, 红黑树是1.8才引入的;- 一个
Segment对象
就是一个段; 当往容器中添加元素调用 put 方法时, 锁的粒度就是一个段; - 调用 put 方法时, 先计算应该放到
Segment[ ]
中的哪个段, 然后调用egment.put()
方法将 Entry 插入到segment.Entry[ ]
,可以看出, 一次插入有两次散列,一次选择段, 一次选择段内的位置; - 两次散列使用的 hash 值不同;
- Segment数组的长度是固定的, 构造以后就不会再扩容, 内部的Entry[ ] 是一个个独立的Map, 会各自扩容, 互相之间的大小没有必然关系;
- ConcurrentHashMap拉链时采用的是头插法;
构造方法
- 默认的 initialCapacity = 16, loadFactor = 0.75, concurrentcyLevel = 16;
- concurrencyLevel 经一些计算, 得到 Segment数组 的长度, 并且不再改变.
具体逻辑为: 取 >= concurrencyLevel 的, 最小的, 2的整数次幂作为 Segment 数组的长度; 原理是将 1 不断左移, 直到 >= concurrencyLevel参数; - initialCapacity 和 concurrencyLevel 经计算得到 segment[0] 中Entry数组的长度;
具体逻辑为:(initialCapacity / concurrentcyLevel)
向上取整得tmp, 如果tmp <= 2, 直接取 capacity = 2; 如果tmp > 2, 再取 >= temp 的, 最小的, 2的整数次幂; - 在默认情况下, 最终 Segment 数组长度为 16, 一个 Segment 内部的 Entry 数组长度为 2;
- 根据计算出的尺寸, 创建一个 Segment 对象 s0;
- 使用
UNSAFE.putOrderedObject(ss, SBASE, s0)
系统调用将 s0 直接放到 segment数组 下标为0的位置; - 经过构造后, 只有segment[0] 的位置有 非null segment对象, 其余位置将在各自首次添加元素的时候复制 segment[0] 的参数, 进行初始化; s0 就是一个原型;
hash方法
- 通过一个 seed 和 key.hashCode() 异或, 再进行移位相加, 异或 等操作得到Hash值
put方法
- 不允许以 null 作为 key;
- ConcurrentHashMap不允许以null作为value, 这是为了防止歧义; 在HashMap中, 串行执行的情况下, 如果 get(key0) == null, 可以使用containsKey方法确定是否存在key0; 而在多线程环境下, 如果使用相同的方式, 即使后面调用containsKey发现存在key0, 那你也不能确定key0是不是在你get之后由别的线程插入的; 也就是说无法确定在你调用get方法时得到 null 的瞬间, 究竟是不存在key0? 还是存在key0但是value == null
- JDK1.8中, ConcurrentHashMap判断两个键值对是否重复的逻辑是 key 的hash值 == 且 (key == 或 equals), 而ConcurrentHashMap的判断逻辑是 key== 或 ( hash== 且 key.equals )
- 在
ensureSegment
方法中, 通过不断判断 ss[k] 上是否已经有 非null 引用来保证线程安全; - 最终结果就是以当前 segments[0] 中保存的大小参数, 来 new 一个 segment 对象, 放入 ss 对应位置, 并将该对象返回
- 在
Segment::put
方法中, 使用锁机制保证线程安全; - static final class Segment<K,V> extends ReentrantLock implements Serializable; Segment类继承了ReentrantLock, 也就是说segment对象本身就可以充当一个锁
- segment内部的put方法, 加锁成功后的逻辑和 HashMap 基本上是一样的, 重点看scanAndLockForPut方法
- 在内部尝试提前创建Node, 创建完成后
扩容机制
- 扩容函数名字是 rehash() ;
- 扩容操作是被包裹在 segment.put 方法内部的锁的作用范围之内的; 所以必然是线程安全的
- 其余的扩容机制和 HashMap 一致
重点 和1.8的区别
- JDK1.8的时候, 不再采用分段的模式, 而是在 HashMap 的基础上, 采用 CAS 操作和 synchronized 实现线程安全;
- JDK1.8 的 ConcurrentHashMap 大体上和 HashMap 是一样的; 区别是:
- 插入的时候, 如果对应位置为null, 使用 CAS 的方式插入; 不是 null 则 synchronized 内完成插入
- 删除的时候, synchronized 内删除;
- 扩容的时候, CAS 的方式, 多线程扩容;
- CHM 为什么换成 JDK1.8 的实现?
- 代码和 HashMap 基本一致, 更清晰简单;
- 采用 CAS 和 synchronized 一起实现线程安全, 插入操作锁的是 Entry 数组的一个下标, 并发度更高了; 并且实现了安全的多线程扩容;