HashMap高频面试知识点

  • HashMap
    • HashMap是基于hash表的一种数据结构,用于存放键值对,核心就是把hash值映射到数组的索引位,通过数组+链表(JDK1.8开始通过数组+链表+红黑树)解决Hash冲突。

      • 因为当hash冲突较多时,链表中元素增加,查询插入等操作的时间复杂度从O(1)------>O(n)
      • 而二叉树的时间复杂度O(log n)
    • 数组下标=(数组长度-1)&hash(key),不同的key可能会产生相同的hash值,从而导致hash冲突,jdk1.7之前链表采用头插法,但是多线程环境下可能导致环路从而引发死循环。Jdk1.8之后采用尾插法。

    • HashMap初始容量16,负载因子0.75,那么阈值就是16*0.75=12,超过12就会触发2倍扩容操作

    • 扩容时,每个元素存储位置将根据新的数组容量重新计算hash值,并移动到新的数组中,这个操作加rehashing;

      • Jdk1.7之前,就是存粹的把所有元素重新hash,然后一个一个搬过去
      • Jdk1.8之后,旧数组长度16即010000,新数组长度32即100000
      • 而数组下标=(数组长度-1)&hash(key),16-1为001111,32-1为011111
      • 所以重点就在于key的hash值从右往左第五位是否为1,如果为1那么久需要搬迁,新数组下标=原下标+旧数组长度16。
      • 如果是0的话,怎么与都是0,所以新数组高位按位与操作不受影响(注意:与操作有0为0,全1为1),那就说明在原位置,不需要迁移。减少了一部分hash计算的开销。
    • 从jdk1.8开始,为了优化hash冲突时的查询性能,

      • 当链表的数量大于等于8并且数组的大小大于等于64,链表就会转换成红黑树。
      • 当数组长度小于64,则选择扩容。
        • 当数组容量较小,过早的转换成红黑树,可能很快又会触发数组扩容,导致没必要过早树化的开销。
        • 另外红黑树比链表更占内存。
      • 树的元素低于6的时候,树就会转换成链表。
    • 常见的处理hash冲突的办法:拉链法,开放寻址法,再哈希法

  • HashMap VS Hashtable
    • 线程安全
      • HashMap 线程不安全,多线程环境下会产生数据一致性问题。
      • Hashtable 线程安全,内部的方法采用了synchronized,保证了多线程并发的安全性。
    • 性能差异
      • HashMap 由于没有线程同步的开销,所以性能优于Hashtable。
      • Hashtable 由于方法的锁机制,性能不如HashMap。
    • 空值null
      • HashMap 允许一个null键,多个null值。
      • Hashtable 键值都不允许null,会报空指针异常。
  • ConcurrentHashMap
    • ConcurrentHashMap是Hashtable的替代方案,读操作的无锁化,写操作的分段锁机制提高了并发的性能,避免了全局锁的瓶颈。性能优于Hashtable。

    • JDK1.7 ConcurrentHashMap

      • 采用的是分段锁,每个Segment是独立的。默认16个,所以最多支持16个并发。
      • 采用数组+链表
      • 原理 :
        • 先通过key的hash判断应该在哪个Segment的数据下标,然后对这个Segment加锁(Segment继承ReentrantLock,自带加锁的能力)。
        • 然后再次通过key的hash得到Segment里HashEntry的数组下标,接下去的步骤同HashMap。
      • 扩容:
        • 扩容时针对单个Segment进行扩容的,不会影响其他的Segment,每个Segment都有自己的负载因子。因此它并不是针对整个ConcurrentHashMap扩容的。
      • Size大小:先尝试不加锁三次计算sum值,如果三次值相同,就直接返回;如果不同就对每个Segment进行加锁计算
      • 缺陷:Segment数组一旦初始化了之后,就不会扩容,导致对并发度控制过于死板(只有HashEntry才会扩容)
      • 结构如下
  • JDK1.8 ConcurrentHashMap
    • 移除了Segement的概念,锁的粒度更加细化,通过CAS进行写入操作,只有在更新链表和红黑树的节点时才会用到synchronized,并且只锁链表或树的头节点(相当于Node数组的每个节点都能上一把锁,并发度更高了)。
    • 数组+链表+红黑树
    • 原理:
      • 先计算key的hash后得到数组下标,如果数组下标没有Node,就通过CAS塞入新的Node。
      • 如果当前数组下标存在Node,那么则先通过Synchronized对这个Node加锁。
      • 然后再判断key是否相等,key相同就替换value, 反之就Hash冲突(操作同HashMap)
    • 扩容:
      • Jdk1.8 取消了Segment,因此整个ConcurrentHashMap数组都会被扩容。通过CAS操作确保线程的安全,避免锁住整个数组,并且扩容时多个线程共同参与,逐步迁移就数据到新数组中。
      • 在putVal()的时候,如果无限循环的内部正在扩容,就调用helpTransfer()帮忙一起扩容,扩容完毕后还会向新的数组中插入元素的。
  • Size大小:jdk1.8中,搞了一个数组CounterCell ,每次put和remove的时候,先通过CAS修改baseCount的值,如果CAS失败,则说明存在线程竞争,就要通过hash到对应的CounterCell数组的下标下维护对应的数量;最终size=baseCount+所有的CounterCell
  • ConcurrentHashMap volicate关键字保证了可见性,使得get的时候不用加锁也能线程安全。
  • ConcurrentHashMap key 和value都不支持null。
相关推荐
虚拟搬运工5 分钟前
Python类及元类的创建流程
开发语言·chrome·python
Code哈哈笑18 分钟前
【C++ 学习】多态的基础和原理(10)
java·c++·学习
chushiyunen23 分钟前
redisController工具类
java
消失的旧时光-194325 分钟前
kotlin的密封类
android·开发语言·kotlin
A_cot29 分钟前
Redis 的三个并发问题及解决方案(面试题)
java·开发语言·数据库·redis·mybatis
学步_技术29 分钟前
Python编码系列—Python原型模式:深克隆与高效复制的艺术
开发语言·python·原型模式
刘某某.34 分钟前
使用OpenFeign在不同微服务之间传递用户信息时失败
java·微服务·架构
alden_ygq35 分钟前
GCP容器镜像仓库使用
java·开发语言
七折困41 分钟前
列表、数组排序总结:Collections.sort()、list.sort()、list.stream().sorted()、Arrays.sort()
java·集合·数组·排序
苹果酱05671 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件