HashMap和ConcurrentHashMap扩容过程
HashMap
存储结构
HashMap是数组+链表+红黑树(1.8)实现的。
(1)Node[] table,即哈希桶数组。Node是内部类,实现了Map.Entry接口,本质是键值对。
下图链表中的Node节点
(2)Node[] table初始化长度为16,负载因子是0.75,threshold是HashMap容纳的最大Node个数,threshold = length * Load factor。
resize扩容
1.7
扩容过程:当键值对大小大于数组大小时进行扩容,数组扩容原来的2倍,然后对键重新rehash。
1.8
使用2次幂的扩展,所以,元素的位置要么是原位置,要么是在原位置再移动2次幂的位置。不需要rehash,只要看原来hash值新增的bit是1还是0就好了,是0的话索引没有变,是1的话索引变成“原索引+oldCap”。省去了hash的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。
有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从DK1.8不会倒置(尾插法)。
ConcurrentHashMap
1.put方法
1.1 数组初始化时的线程安全
数组初始化时,首先通过自旋保证一定可以初始化成功,然后通过CAS设置SIZECTL变量的值,保证同一时刻只能
有一个线程对数组进行初始化,CAS成功之后,会再次判断当前数组是够初始化完成。通过自旋 + CAS + 双重
check保证了数组初始化时的线程安全。
1.2 新增槽点值时的线程安全
-
通过自旋死循环保证一定可以新增成功。
-
当前槽点为空时,通过CAS新增。
-
当前槽点有值,锁住当前槽点。
put 时,如果当前槽点有值,就是 key 的 hash 冲突的情况,此时槽点上可能是链表或红黑树,我们通过锁住槽点,来保证同一时刻只会有一个线程能对槽点进行修改。
- 红黑树旋转时,锁住红黑树的根节点,保证同一时刻,当前红黑树只能被一个线程旋转。
以上4点保证在各种情况下,都是线程安全的,通过自旋 + CAS + 锁。
1.3 扩容时的线程安全
ConcurrentHashMap 扩容的方法交transfer,思路:
- 首先把老数组的值全部拷贝到扩容的新数组上,从数组的队尾开始拷贝;
- 拷贝数组的槽点时,先把原数组槽点锁住,保证原数组槽点不能操作,拷贝到新数组时,把原数组槽点复制为转移节点;
- 这时如果有新数据正好put到此槽点时,发现槽点为转移节点,就会一直等待,所以在扩容完成之前,槽点对应的数据不会变化;
- 从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组中的节点设置为转移节点;
- 直到所有数组数据都拷贝到新数组时,直接把新数组整个复制给数组容器,拷贝完成。