CloudNativeEra
  • Introduction
  • 名词解释
  • Computer Science
    • Computer Organization
      • CPU
      • 二进制、电路、加法器、乘法器
      • 编译、链接、装载
      • 存储器
      • IO
    • Operating System
      • 操作系统基础知识
      • 系统初始化
      • 进程管理
      • Everything about Memory
      • 文件系统
      • 并行编程
      • Linux
        • CPU
        • IO 多路复用
        • DMA IO and Linux Zero Copy
    • Computer Network
      • 网络相关命令
      • 评估系统的网络性能
      • 网络抓包
      • Linux 最多支撑的 TCP 连接
      • 网络虚拟化
      • DHCP 工作原理
    • Data Structure and Algorithm
      • 题目列表
      • Summarize
        • 方法总结
        • 二分思想
        • 树的演化
        • 算法思想总结
      • Data Structure
        • Data Struct - Array
        • Tree
        • Heap
        • Hash
        • 字符串
      • Algorithm
        • Sorting Algorithm
        • 查找
        • 贪心算法
        • 动态规划
        • 位运算
      • Practice Topics
        • Data Struct in SDK
        • Topic - Tree
        • Topic - Graph
        • Topic - 滑动窗口
        • 剑指 Offer 题解
    • 并发编程
      • 并发模式
      • 并发模型
  • 系统设计
    • 软件设计
      • 软件架构
      • 编程范式
      • 系统设计题
      • 设计原则
      • 计算机程序的构造和解释 SICP
    • 领域驱动设计
      • 应用:在线请假考勤管理
      • 应用: library
    • 微服务与云原生
      • Designing and deploying microservices
      • 容器技术
      • Docker
      • Etcd
      • Kubernetes
        • Kubernetes - Mapping External Services
      • Istio
      • 监控
    • 分布式系统
      • 分布式理论
      • 分布式事务
    • 后端存储设计
      • 缓存设计
      • 数据库架构设计
    • CI/CD
    • 设计最佳实践
    • 测试
    • 安全
    • 综合
      • 开发实践
      • 分布式锁
      • 分布式计数服务
      • 弹幕系统设计
      • 消息队列设计
      • 分布式ID生成算法
      • 限流设计
      • 网关设计
      • 通用的幂等设计
      • 分布式任务调度
        • Timer
        • ScheduledExecutorService
        • Spring Task
        • Quartz
      • 交易系统
      • 权限设计
  • 编程语言
    • 编程语言
    • C & C++
    • Java
      • JVM
        • JVM Bytecode
      • Java 核心技术
      • Java 8 新特性
      • Java 集合框架
      • Java NIO
      • 并发编程
        • 线程生命周期与线程中断
        • 三个线程交替打印
        • 两个线程交替打印奇偶
        • 优雅终止线程
        • 等待通知机制
        • 万能钥匙:管程
        • 限流器
        • 无锁方案 CAS
    • Java 源码阅读
      • Unsafe
      • 异步计算 Future
      • Java Queue
      • CoalescingRingBuffer 分析
      • Java Collections
        • PriorityQueue 分析
        • HashMap 分析
        • TreeMap
    • Golang
    • Python
  • 框架/组件/类库
    • Guava
      • Guava Cache
      • Guava EventBus
    • RxJava
    • Apache MINA
    • Netty
      • 网络 IO 模型
      • Netty 生产问题
    • Apache Tomcat
    • MyBatis
    • 限流框架
    • Spring Framework
      • Spring Core
      • Spring 事务管理
    • Spring Boot
    • Spring Cloud
      • Feign & OpenFeign
      • Ribbon
      • Eurake
      • Spring Cloud Config
    • FixJ
    • Metrics
    • Vert.x
  • 中间件
    • Redis
      • Redis 基础
        • Redis 数据结构设计与实现
        • Redis 高性能网络模型
      • Redis checklist
      • 应用案例 - Redis 数据结构
      • 应用案例 - Redis 缓存应用
      • 应用案例 - Redis 集群
      • Redis 客户端
      • Redis 生产案例
        • [译] 在 Redis 中存储数亿个简单键值对
    • MySQL
      • MySQL 基础
      • MySQL Index
      • MySQL Transaction
      • MySQL 优化
      • MySQL 内核
      • MySQL Command
      • MySQL Checklist
      • MySQL Analysis Tool
      • 实现 MySQL
    • State Machine
    • 数据库连接池
    • MQ
      • 高性能内存队列 Disruptor
      • Kafka
      • Pulsar
      • RocketMQ
        • Broker 的设计与实现
      • NSQ
  • 实际案例
    • 线上 Case
      • Request Aborted
      • MySQL - Specified key was too long
      • Java 应用 CPU 100% 排查优化
      • 频繁 GC 导致的 Java 服务不响应
      • 导出优化
  • 大数据
    • 流计算
    • Flink
  • 其他
    • 工具
    • 读书
      • 设计数据密集型应用
      • 实现领域驱动设计
      • 精通比特币
      • 提问的智慧
    • 论文
    • 工程博客
    • 阅读源码
    • 面试
      • 如何在最短的时间里对对方有个全面的了解
    • 分享
    • 软技能
    • Todo
  • Blog
    • #算法
      • 查找
      • 位运算
      • 树
    • #架构
      • 1- 通信
    • Design & Dev & Opt
      • High Performance Data structure Design
  • Tiny Project
    • A Simple WeChat-like Instant Messaging Platform
由 GitBook 提供支持
在本页

这有帮助吗?

  1. 编程语言
  2. Java 源码阅读
  3. Java Collections

HashMap 分析

#Hash #HashMap

Java 中的 HashMap 是散列表的实现,有必要 review 一下一个工业级的散列表实现要考量的 Key Point:

  1. 散列表的三个基本元素:key、散列函数、value, key 通过散列函数计算得到 value 存储的位置

  2. 设计散列函数

  3. 控制散列表的装载因子,初始大小和动态扩容策略

  4. 有效解决散列冲突

  5. 对一个工业级散列表的实现要遵守几点:首先支持快速查询、删除、插入等操作;其次,内存占用要合理,不要过多浪费内存;最后,性能稳定,极端情况下,散列表的性能也不能退化到无法接受

总之,我们在结合使用场景和具体业务数据考虑设计散列表实现时,要抓住上面的 key point;接下来,从这几个点来验证一下 Java 中的 HashMap 实现

  • HashMap 的散列函数设计

散列函数的设计要点是:首先,不能复杂,太复杂的散列函数也会影响程序性能;其次,散列函数计算后得出的值要尽可能随机和均匀。

static final int hash(Object key) {
    int h;
    // 先取对象 key 的 hashCode
    // 最后返回 h ^ (h >>> 16),将hashCode的高16位移到低16位,然后做异或,保证了很好的随机性,位运算本身是很高效的
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 在插入元素时计算key要插入的元素位置
// 通过 (n - 1) & hash 来计算位置,n 代表数组的 capacity,hash & (n - 1) 相当于 hash % n, 也就是除留余数法
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);


// Object 的 hashCode 返回的是 int
public native int hashCode();

可见,HashMap 的散列函数设计分为两步,先对 key 求hash值,然后再根据 HashMap 的节点(本质是一个数组)容量来计算数组中的位置;求 key hash值的方式简单高效,位移后异或随机且高效,所以总体非常符合散列函数的设计要求。( 注:A % B = A & (B - 1) )

  • 控制散列表的装载因子,初始大小和动态扩容策略

HashMap 的实现底层用了数组,数组有容量范围的;装载因子表示了空槽位的多少,计算方式为:已经使用的槽位数/散列表的总长度。 从装载因子我们可以得出一些结论:当装载因子越大,散列表里的元素就越多,空闲位置就越少,散列冲突的概率就越大,会导致整个插入和查找的时间变长,最糟糕的情况会退化成链表的查找和插入时间复杂度,这是不可忍受的。这个时候就需要进行动态扩容策略来调整装载因子,让他不会太大;

但是当装载因子太小时,会导致浪费内存空间,所以保证装载因子在一个合理的值是比较重要的。

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 初始容量大小为16,如果没指定的话

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 装载因子默认是 0.75

// 从 Node 的定义来看,HashMap 采用的是链表法存储元素
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    // 指向下一个和当前元素有相同位置索引的元素
    Node<K,V> next;
    // ... 省略了部分代码
}
final Node<K,V>[] resize() {
    // .... 省略了部分代码
    threshold = newThr;
    // 新的容量的计算策略是如果初始为0,就用默认的16大小,如果大于16小于2^30,每次扩容就double size
    // 这里使用新的容量申请了一个新的数组
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 老数组如果有元素的话,需要做copy且重新计算索引位置
    if (oldTab != null) {
        // 遍历老数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 不等于null表示元素是一个存在的合法元素,需要copy
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果当前元素没有后继元素,计算新的位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode) 
                    // 如果是树节点类型,这里是红黑树,将树中的节点做分裂,因为容量变化,所以节点计算出的位置索引也会发生变化
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 查找链表中的每一个元素,来计算他们新的位置
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        // 使用原来的位置
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 使用+oldCap的位置
                        newTab[j + oldCap] = hiHead;
                    }
                    // 这里的位置计算有个数学上的技巧,扩容后新元素的位置要么在原来的位置,要么在原来的值+oldCap的位置
                }
            }
        }
    }
}    

// 启动扩容的地方,当元素个数大于阈值的时候会触发
// 其中 threshold = Capacity * 装载因子,一切就联系起来了
if (++size > threshold)
    resize();

从源代码分析可见,工业级散列表的实现还是比较复杂的,但是原理和思想才是最重要的: 1. 首先分配一个初始容量,当然也可以通过构造函数来指定,固定一个装载因子,计算出触发扩容的阈值 2. 在每次插入时,判断元素数量是否到达阈值,到了就触发扩容策略 3. 扩容策略是在原来的基础上double,如果没有指定或者为0就使用默认初始容量16 4. 扩容的过程就是新申请一个更大容量(原来的2倍)的数组,将老数据copy过去,重点是位置索引的重新计算,然后是否老数组 5. HashMap 的实现使用了链表法,后面会分析到,还会涉及到红黑树的分裂,这个过程就比较复杂了

  • 有效解决散列冲突

无论设计的多么好的散列函数,都会发生散列冲突,当冲突的时候就必须解决散列冲突,常见的方法有:开放寻址法和链表法。

HashMap 采用的方式是链表法

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
// 使用数组来存放 Node
transient Node<K,V>[] table;

// 从 Node 的定义来看,HashMap 采用的是链表法存储元素
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;  // 存放Key
    V value;  // 存放 Value
    // 指向下一个和当前元素有相同位置索引的元素
    Node<K,V> next;
    // ... 省略了部分代码
}

但是,当某个位置的链表过长时,会造成查找和插入的时间复杂度增加,HashMap 在这里做了一个优化,当链表长度超过某个值时,就将链表转换成红黑树,当红黑树的元素个数小于某个值时,会转成链表.

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

所以在插入、查找、删除等操作时,HashMap 可以提供比较高的效率,基本都是 O(1) 的时间复杂度。这得以于 HashMap 在各方面的优化:

  1. 对散列函数的优化,使用自定义的hash函数重新计算hash,计算hash的时候使用位操作,效率更高

  2. 对扩容策略的优化,扩容带来散列冲突概率的降低

  3. 对散列冲突的优化,当链表性能下降时,转成红黑树,红黑树加速了查找、插入的速度

  4. etc.

还可以参考:

上一页PriorityQueue 分析下一页TreeMap

最后更新于4年前

这有帮助吗?

盗用一张图,就是这样的

Map 综述(一):彻头彻尾理解 HashMap
Map 综述(二):彻头彻尾理解 LinkedHashMap