ConcurrentHashMap 可以实现线程安全。在1.8之前和之后实现线程安全的方法不同。
1.8 之前
将 table 数组划分为多个 segment ,对每一个 segment 单独加锁,不同的 segment 可以跨线程运算,提高并发效率。
每一个segment有一个单独的table数组,类似1.7版本的 hashmap,table数组遇到哈希冲突后,同hash值的元素使用的数据结构是链表。
读是不需要加锁的,因为 segment 的 table 数组加了 volatile 修饰,HashEntry 的val和next字段也用 volatile 修饰。 一个线程改变了值,另一个线程能马上发现。
put的时候需要加锁,当put时,所在的segment还没有创建,会懒加载创建segment。实例化segment之后,会通过cas插入到segment数组中,保证线程安全。
下面是 1.8之前 ConcurrentHashMap 的 Segment 的 put 流程
1 | final V put(K key, int hash, V value, boolean onlyIfAbsent) { |
1.8 之后
1.8之后ConcurrentHashMap改变了加锁策略,改为对哈希数组的每一个hash位置加锁;同时,将哈希冲突的链表替换为了和 HashMap 一样的红黑树。
sizeCtl
- 为0,默认状态,代表数组未初始化, 且数组的初始容量为16
- 为-1,表示数组正在进行初始化
- 为正数,其记录的是数组的扩容阈值
- 小于0,并且不是-1,表示数组正在扩容, -(1+n),表示此时有n个线程正在共同完成数组的扩容操
put时如何保证线程安全
- 在put时对table哈希数组的每一个hash位置,如果为该位置为null,cas判断,然后在改位置添加元素;如果该位置不为null,则对该hash位置进行Synchronize加锁,将新元素加载链表or红黑树上。
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
初始化hash数组
- 在初始化哈希数组的时候,会cas+自旋保证线程安全
1 | /* 初始化底层数组 */ |
扩容
ConcurrentHashMap 的扩容在transfer方法中;
需要注意的是,ConcurrentHashMap 有一个多线程协助扩容的机制。
- 当一个线程在扩容的时候,在移动hash数组的元素到新数组的时候,会确定自己的开始位置 和 一个stride容量,hash数组stride内的元素自己负责扩容拷贝;然后确定一个 transferIndex 作为下次拷贝的起点。 同时在原数组的hash位置放置一个 ForwardingNode 。
- 当有新的线程想要在这个hash位置插入元素时,发现这里有一个 ForwardingNode ,会协助一起扩容,帮助将原数组的元素迁移到新数组。 以 transferIndex 作为起点,stride容量内的元素自己负责协助拷贝
- 对于线程安全,在扩容拷贝的时候,会对hash数组要拷贝的位置进行 synchronized 加锁。在计算 transferIndex 的会使用 cas+自旋 机制保证线程安全。
多线程协助扩容触发的时机:
- 当添加元素时,发现添加的元素对用的桶位为 fwd 节点,就会先去协助扩容,然后再添加元素
- 当添加完元素后,判断当前元素个数达到了扩容阈值,此时发现sizeCtl的值小于0,并且新数组不为空,这个时候,会去协助扩容
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |