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。
相关推荐
奶香臭豆腐12 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
晚夜微雨问海棠呀20 分钟前
长沙景区数据分析项目实现
开发语言·python·信息可视化
graceyun20 分钟前
C语言初阶习题【9】数9的个数
c语言·开发语言
hanbarger22 分钟前
mybatis框架——缓存,分页
java·spring·mybatis
cdut_suye30 分钟前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
苹果醋342 分钟前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行43 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园1 小时前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法
Swift社区1 小时前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift