Java HashMap底层原理、源码解析以及面试常见问题

154 阅读3分钟

题外话:如何学习

逻辑思维能力是梳理学习方法的基础。养成线性思维:两个或者多个概念,像一条线串起来。

演绎推导法

示例:因果推理。因为JAVA中网络编程只提供了BIO和NIO两种方式,所以一切框架中,涉及到网络处理的,都可以用这两个知识点去探究原理。

归纳总结法

示例:可能正确的猜想。线上10台服务器,有三台总是每天会自动重启,收集相关信息后,发现是运维在修改监控系统配置的时候,漏掉了提高这三台机器的重启阈值。

类比法

  1. 集群:就好像是马在拉车,一匹马拉不动的时候,就使用多匹马去拉。
  1. 单台服务器承受不来的 QPS,由多台服务器去均衡
  1. 分布式:就像是理发的过程中,洗头发和剪头发是不同的人负责的。
  1. 一个用户的请求,由多个系统协同完成


深入理解 HashMap

image.png

实现推理

数据要存储

  1. 涉及到数据结构:数组、链表、栈、树、队列

数组的插入和查找

➢ 顺序查找:插入时按先后顺序插入,查找时轮询扫描进行比对。

➢ 二分查找:插入时进行排序;查找时将n个元素分成大致相等的两部分,减少复杂度。

➢ 分块查找:分块查找是二分查找和顺序查找的一种改进。

➢ 哈希表:对元素的关键信息进行hash计算,求出下标后直接插入或查找。常用的实现是除留余数法。

put方法

JDK 1.7 实现

  1. 使用的是哈希表
  2. 默认的 capacity 为 16,负载因子 0.75
  3. 如果key为null,则放到 table[0]
  4. 先 key 取 hash,然后对当前的数组长度取模
  1. 这里,看如果未指定 capacity,则默认为16.
  1. 然后放,如果hash冲突,比较equals。相同则覆盖
  2. 放之前会检查大小,如果当前大小达到了threshold,则扩容到 capacity * 2
  1. threshold 默认为 capacity * 0.75
  2. 在这个过程中,会遍历每一个数组,进行 rehash ,在 transfer 方法中。
  1. 如果通过了,则 new 一个 entry 放入
  1. 这里,如果 hash 冲突了,则会生成一个含有 next 指针的链表

JDK 1.8 改进

  1. 当链表长度达到
    TREEIFY_THRESHOLD(8)
    以后,会转换为红黑树
  2. resize 扩容操作,不再是 1.7 的达到 threshold && 数组当前位置有 entry,改成了每次 put 完成,统计 modCount 以后

其他

为什么要有 threshold

  • 当 size = threshold 后,就需要扩容
  • 因为 size 越大,代表 hash 冲突越多,遍历 数组的时间复杂度是 O(1),但遍历链表,则为 O(n)

threshold 都什么时候变化

  • 构造函数中,默认为 16
  • 第一次 put 的时候,会变为 capacity * loadFactor
  • 超过后,为扩容后的大小 * loadFactor

是否是达到 threshold 就会扩容

  • 不是,需要达到 threshold && 通过 key hash 取模的位置上,不为null

为什么 hashMap 线程不安全

  • 因为在 createEntry 的时候,有size ++
  • ++ 本来就不是原子操作
  • 如果达到了 threshold,会出问题

为什么临界值是 8 的时候转为红黑树

  1. 少量数据的时候,链表查找方便。而树结构徒增性能损耗

TODO

  • 顺序查找、二分查找、分块查找、哈希表、红黑树