深入解析Java HashMap的高性能扩容机制与树化优化

简介: 深入解析Java HashMap的高性能扩容机制与树化优化

深入解析Java HashMap的高性能扩容机制与树化优化

Java中的HashMap是一个基于哈希表实现的键值对(key-value)存储数据结构。它属于Java Collections

Framework的一部分,用于高效地存储和检索数据。以下是对Java HashMap的一些详细探讨:

基本特性

  • 键值对存储:HashMap存储键值对,每个键对应一个值。键和值都可以是任意类型的对象。
  • 键的唯一性:HashMap中的键是唯一的,即不允许有重复的键。若尝试存储一个已经存在的键,其对应的值将被新的值替换。
  • 无序存储:HashMap不保证键值对的顺序,这意味着存储的顺序与遍历的顺序可能不同。

主要操作

  • put(K key, V value):将指定的键值对插入到HashMap中。如果键已经存在,则更新对应的值。
  • get(Object key):根据键获取对应的值,如果键不存在,则返回null。
  • remove(Object key):移除指定键的键值对。
  • containsKey(Object key):判断HashMap中是否包含指定的键。
  • containsValue(Object value):判断HashMap中是否包含指定的值。
  • size():返回HashMap中键值对的数量。
  • isEmpty():判断HashMap是否为空。

内部工作原理

HashMap的核心在于哈希表(hash table)实现。以下是其基本工作流程:

  • 哈希函数:通过键的hashCode()方法计算哈希值,然后用哈希值对数组长度取模,确定键值对在哈希表中的位置。
  • 冲突处理:当不同的键计算得到的哈希值相同时,会发生哈希冲突。Java使用链表法(即链地址法)来处理冲突:在哈希表的每个位置上,实际存储的是一个链表,所有哈希值相同的键值对都存储在该链表中。
  • 扩容机制(最后部分重点解释):HashMap有一个负载因子(默认0.75),当实际存储的键值对数量超过capacity * loadFactor时,HashMap会进行扩容(通常是原来容量的两倍),并重新散列所有的键值对到新的哈希表中。

优缺点

优点

  • 快速存取:在理想情况下,HashMap的插入、删除和查找操作的时间复杂度为O(1)。
  • 灵活性:HashMap允许使用null值和null键,这在某些应用场景下非常灵活。

缺点

  • 非线程安全:HashMap不是线程安全的。如果多个线程同时操作同一个HashMap实例而没有适当的同步措施,会导致数据不一致。线程安全请参考全面解读CourrentHashMap
  • 性能退化:在极端情况下(如所有键的哈希值都相同),HashMap的性能会退化为O(n)。

扩容机制(结合treeifyBin方法)

在Java 8中,HashMap在处理哈希冲突时引入了树化机制。当某个桶中的链表长度超过一定阈值时(默认是8),链表会被转换成红黑树,以提高查询效率。这一过程被称为树化(treeify),而具体的树化操作则是在treeifyBin方法中进行的。下面我们结合treeifyBin方法和扩容过程来详细讲解。


treeifyBin 方法

首先,让我们看一下treeifyBin方法的实现:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

关键点分析
  • 树化的触发条件
    当某个桶中的链表长度超过阈值(默认8)时,会触发树化。但是,在进行树化之前,会检查哈希表的容量。如果当前哈希表的容量小于最小树化容量(默认64),则会进行扩容而不是树化:
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();

MIN_TREEIFY_CAPACITY的默认值是64。这意味着,如果当前哈希表的容量小于64,即使链表长度超过阈值,也不会进行树化,而是优先扩容。这是为了避免在容量较小的哈希表中引入树结构,进而影响性能。

  • 链表转换为红黑树

如果哈希表的容量大于或等于最小树化容量,会将链表节点转换为红黑树节点:

else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do {
        TreeNode<K,V> p = replacementTreeNode(e, null);
        if (tl == null)
            hd = p;
        else {
            p.prev = tl;
            tl.next = p;
        }
        tl = p;
    } while ((e = e.next) != null);
    if ((tab[index] = hd) != null)
        hd.treeify(tab);
}

这里,首先遍历链表,将每个普通节点(Node)转换为红黑树节点(TreeNode)。然后,将转换后的树节点构建成一个双向链表,最后调用红黑树节点的treeify方法将链表转换为红黑树。

  • replacementTreeNode 方法
  • 该方法用于将普通节点转换为红黑树节点:
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next, null);
}
  • treeify方法
    红黑树节点的treeify方法将链表转换为红黑树:
final void treeify(Node<K,V>[] tab) {
    // Implementation details...
}


扩容与树化的关系

在HashMap中,扩容与树化是两种不同的性能优化手段。当哈希表的容量不足时,扩容是首选的优化手段,而当哈希表容量足够大且某个桶中的链表过长时,才会进行树化操作。扩容的主要目的是减少哈希冲突,从而降低链表长度,而树化则是通过将链表转换为红黑树来提高查找效率。


扩容过程中处理树节点

  • 在扩容过程中,如果旧哈希表中某个桶已经是红黑树结构,那么在将这些节点重新哈希到新哈希表时,需要保持红黑树结构。这一点在扩容的resize方法中有体现:
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) // 初始阈值
        newCap = oldThr;
    else {               // 使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            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;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

TreeNodesplit 方法

TreeNodesplit方法将当前红黑树节点分割为两个链表,一个保留在原位置,另一个移动到新位置:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // 两个新的链表头
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    if (loHead != null)
        tab[index] = loHead;
    if (hiHead != null)
        tab[index + bit] = hiHead;
    // 将链表转换为红黑树
    if (lc <= UNTREEIFY_THRESHOLD)
        tab[index] = loHead.untreeify(map);
    else if (loHead != null)
        loHead.treeify(tab);
    if (hc <= UNTREEIFY_THRESHOLD)
        tab[index + bit] = hiHead.untreeify(map);
    else if (hiHead != null)
        hiHead.treeify(tab);
}

在扩容过程中,split方法会将当前红黑树节点分成两部分,然后判断链表长度是否低于树化阈值(UNTREEIFY_THRESHOLD,默认6)。如果低于阈值,则将红黑树退化为链表;否则,保持红黑树结构。


总结

通过treeifyBin方法和resize方法的源码分析,可以看出Java 8中HashMap在处理哈希冲突和扩容方面的优化手段:

  • 树化:当桶中链表长度超过阈值时,将链表转换为红黑树,以提高查询效率。
  • 扩容优先:如果哈希表容量不足,则优先进行扩容,而不是树化,以避免在小容量时引入树结构。
  • 扩容处理树节点:在扩容过程中,保留红黑树结构,并根据新链表长度决定是否退化为链表。
    这些机制共同保证了HashMap在处理大量数据时的高效性。理解这些实现细节有助于在实际使用中优化HashMap的性能。
目录
相关文章
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
471 4
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
686 3
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
371 0
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
820 0
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
645 32
AQS:Java 中悲观锁的底层实现机制
|
Java 区块链 网络架构
酷阿鲸森林农场:Java 区块链系统中的 P2P 区块同步与节点自动加入机制
本文介绍了基于 Java 的去中心化区块链电商系统设计与实现,重点探讨了 P2P 网络在酷阿鲸森林农场项目中的应用。通过节点自动发现、区块广播同步及链校验功能,系统实现了无需中心服务器的点对点网络架构。文章详细解析了核心代码逻辑,包括 P2P 服务端监听、客户端广播新区块及节点列表自动获取等环节,并提出了消息签名验证、WebSocket 替代 Socket 等优化方向。该系统不仅适用于农业电商,还可扩展至教育、物流等领域,构建可信数据链条。
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
1225 1
|
缓存 Dubbo Java
理解的Java中SPI机制
本文深入解析了JDK提供的Java SPI(Service Provider Interface)机制,这是一种基于接口编程、策略模式与配置文件组合实现的动态加载机制,核心在于解耦。文章通过具体示例介绍了SPI的使用方法,包括定义接口、创建配置文件及加载实现类的过程,并分析了其原理与优缺点。SPI适用于框架扩展或替换场景,如JDBC驱动加载、SLF4J日志实现等,但存在加载效率低和线程安全问题。
880 7
理解的Java中SPI机制
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
1053 3
|
存储 设计模式 Java
重学Java基础篇—ThreadLocal深度解析与最佳实践
ThreadLocal 是一种实现线程隔离的机制,为每个线程创建独立变量副本,适用于数据库连接管理、用户会话信息存储等场景。
598 5

推荐镜像

更多
  • DNS