HashMap和ConcurrentHashMap扩容过程

HashMap

存储结构

HashMap是数组+链表+红黑树(1.8)实现的。

(1)Node[] table,即哈希桶数组。Node是内部类,实现了Map.Entry接口,本质是键值对。

下图链表中的Node节点

image

(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 新增槽点值时的线程安全

  1. 通过自旋死循环保证一定可以新增成功。

  2. 当前槽点为空时,通过CAS新增。

  3. 当前槽点有值,锁住当前槽点。

put 时,如果当前槽点有值,就是 key 的 hash 冲突的情况,此时槽点上可能是链表或红黑树,我们通过锁住槽点,来保证同一时刻只会有一个线程能对槽点进行修改。

  1. 红黑树旋转时,锁住红黑树的根节点,保证同一时刻,当前红黑树只能被一个线程旋转。

以上4点保证在各种情况下,都是线程安全的,通过自旋 + CAS + 锁。

1.3 扩容时的线程安全

ConcurrentHashMap 扩容的方法交transfer,思路:

  1. 首先把老数组的值全部拷贝到扩容的新数组上,从数组的队尾开始拷贝;
  2. 拷贝数组的槽点时,先把原数组槽点锁住,保证原数组槽点不能操作,拷贝到新数组时,把原数组槽点复制为转移节点;
  3. 这时如果有新数据正好put到此槽点时,发现槽点为转移节点,就会一直等待,所以在扩容完成之前,槽点对应的数据不会变化;
  4. 从数组的尾部拷贝到头部,每拷贝成功一次,就把原数组中的节点设置为转移节点;
  5. 直到所有数组数据都拷贝到新数组时,直接把新数组整个复制给数组容器,拷贝完成。

Reference

https://tech.meituan.com/2016/06/24/java-hashmap.html

posted @ 2021-09-27 21:22  csgopher  阅读(214)  评论(0编辑  收藏  举报