HashMap vs ConcurrentHashMap

163 阅读7分钟

HashMap

  1. hashmap 特点

    非线程安全的,无序的,基于 map 接口,允许 null 值(把 null 当成普通的值来存,不允许有 null key 重复),底层是 1.7 数组+链表,默认长度为 16,负载因子为:0.75。 如果链表长度大于 8 的话,就变链表为红黑树

  2. 线程安全的实现方式

    • Collections.synchronizedMap(new HashMap)

    • ConcurrentHashMap

  3. hash 碰撞

    碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中 Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法

    链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位; 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。

  4. 负载因子和初始容量

    负载因子:比如说当前的容器容量是16,负载因子是 0.75,``16*0.75=12`,也就是说,当容量达到了12的时候就会进行扩容操作。

    当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

  5. 负载因子什么是 0.75?(时间和空间的权衡)

    源码注释: 负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

  6. 拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?

    之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,链表长度低于6,就把红黑树转回链表,因为根本不需要引入红黑树,引入反而会慢。

  7. 什么时候扩容?

    当数量大于 默认容量*负载因子的时候就会进行扩容:会进行 rehash+复制数据,十分的消耗性能

  8. 1.7 put 方法

    1. 判断当前数组是否需要初始化。
    2. 如果 key 为空,则 put 一个空值进去。
    3. 根据 key 计算出 hashcode。
    4. 根据计算出的 hashcode 定位出所在桶。
    5. 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
    6. 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
  9. 1.7 get 方法

    1. 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
    2. 判断该位置是否为链表。
    3. 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
    4. 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
    5. 啥都没取到就直接返回 null
  10. 1.8 put

    1. 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
    2. 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
    3. 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
    4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
    5. 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
    6. 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
    7. 如果在遍历过程中找到 key 相同时直接退出遍历。
    8. 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
    9. 最后判断是否需要进行扩容。
  11. 1.8 get

    1. 首先将 key hash 之后取得所定位的桶。
    2. 如果桶为空则直接返回 null 。
    3. 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
    4. 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
    5. 红黑树就按照树的查找方式返回值。
    6. 不然就按照链表的方式遍历匹配返回值。
  12. 为什么要转成红黑树

    当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)

  13. map 遍历方法

    1. entrySet 同时获取 key 和 value
    2. 使用 keyset
    3. 使用 java8 的foreach 传入 BitConsumer
  14. hashmap 是怎么 rehash 的?和 redis rehash 的方法有什么不同?

    在扩容的时候进行 rehash,当map中的元素数量达到阈值时,会重新创建一个新的数组,长度为旧数组的两倍(如果长度没有达到上限的话),这时会依次对旧数组(包括其中的链表)按顺序重新计算索引插入,之后重新赋值引用即可

    redis 的 rehash 是渐进式的,redis 会在rehash的同时,保留新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循环渐进地将旧hash的内容一点点地迁到新的hash结构中。当搬迁完成了,就会使用新的hash结构取而代之。当hash移除最后一个元素后,该数据结构自动删除,内存被回收。

  15. hashmap 为啥不是线程安全的?TODO

    coolshell.cn/articles/96…

    并发下的 rehash 是不安全的 容易造成 Infinite Loop。

  16. 为什么使用红黑树代替链表?

    遍历查询链表的效率太低了。O(N) 红黑树O(logN)

ConcurrentHashMap

  1. 为什么是线程安全的?

    1.7 前是数组加链表。 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

    1.8 后是抛弃了 segment 分段锁,使用 CAS+ synchronized来保证并发安全性。

    为什么 1.8 进行了优化? 一是 1.7 并发较少,1.8 数据结果上也进行了调整(链表查询的速度慢)。