Java中使用HashMap时指定初始化容量性能一定会更好吗?

简介: 可以看出,容量16是个分水岭,当容量为16时,二者几乎没啥差异,这也很容易理解,当不指定容量时默认初始容量就是16。但容量大于16时,指定容量时的性能会高于不指定时的性能,随着数量的增加,前者会比后者性能高出50%。但当数据量小于16时,不指定容量大小反而性能更高,最多甚至相差2倍,这就和我们之前的认知不一样了。

一些Java编程老手在做CodeReview时,都会告诉其他人,使用HashMap时建议指定容量大小,原因是指定容量后,代码性能会更好一些。后来随着阿里Java开发手册在业内广为传播,这一点早已深入人心,我自己也早已习惯在使用HashMap时指定容量大小。但我今天突发奇想,想知道指定容量和不指定容量时性能究竟有多少的差异,测试部分测试数据的结果让我大跌眼睛,有些情况下指定容量的性能还比不指定容量时差!! ,但其他部分还是很符合我之前的认知的。


  先说下我的测试平台和测试方法,我使用了openjdk17和jmh单线程测试,测试代码如下:


@Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 2, time = 5)
    @Threads(1)
    @Fork(0)
    @Warmup(iterations = 1, time = 5)
    public void withoutCap() {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < CAP; i++) {
            map.put(random.nextInt(), 1);
        }
    }
    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 2, time = 5)
    @Threads(1)
    @Fork(0)
    @Warmup(iterations = 1, time = 5)
    public void withCap() {
        Map<Integer, Integer> map = new HashMap<>(CAP);
        for (int i = 0; i < CAP; i++) {
            map.put(random.nextInt(), 1);
        }
    }

 这里为了避免Java中小数据缓存,我特意使用了随机数作为KEY,而VALUE一视同仁都使用了1。两个方法就是新建一个HashMap并不断往map里put数据,唯一差异就是一个指定了CAP参数。 在我设置了不同参数后,得到了以下数据(越高越好):


数据量 不指定容量(ops/s) 指定容量(ops/s)

2 51095433 24000032

4 25161756 11813275

8 10767176 5900641

16 2978374 2987958

32 1231637 1545394

64 567643 764260

256 129350 185540

1024 27475 35799

1025 27195 68466

4096 6681 9937

32768 807 1177

65536 377 567

 可以看出,容量16是个分水岭,当容量为16时,二者几乎没啥差异,这也很容易理解,当不指定容量时默认初始容量就是16。但容量大于16时,指定容量时的性能会高于不指定时的性能,随着数量的增加,前者会比后者性能高出50%。但当数据量小于16时,不指定容量大小反而性能更高,最多甚至相差2倍,这就和我们之前的认知不一样了。

 上面数据中还有个很奇怪的点,那就是当数据量为1025时,性能居然还高于1024,而且差异巨大。就好比别人比你多干了1份活,但用的时间比你少一半。我跑了多次都是这个结果,这不是测试误差,这个结果和计算机底层存储实现有关,具体原理可以参考问题 为什么转置512x512的矩阵比转置513x513的矩阵慢?


备注:以上数据经过多次运行测试,数据虽有波动,但数据波动基本都在3%以内。


  那为什么在大数据量的情况下,指定容量的代码性能会更好呢?这就得说到HashMap的实现原理,更详细内容可以参考我之前写的HashMap源码浅析。这里为了方便大家直观地理解性能差异产生的原因,我们用牧场养羊类比下。 假设你要开始养羊,你得现有场地吧,假设你先找了块小场地,但随着你的羊群发展壮大,场地不够用了,你就得搬到一个更大的新场地,如果发展速度特别快,你就得频繁搬家,搬家就逐渐变成了负担。但如果你一开始就知道你最多能养多少的羊,直接找个足够大的场地,不就能省去一直搬家的成本了吗!

 这里你把羊类比成数据,场地类比为内存,在HashMap中,如果开始不指定容量大小,JVM默认会给你一个非常小的(16)的容量空间,如果之后数据量变多,就需要重新申请更大的空间,并把数据迁移到新空间上,于是额外增加了时间消耗。这便是性能差异产生的原因。


  但当容量小于16时,指定容量的方式反而性能更差。这个我之前从未看过其他资料有说过,我简单谈下自己的分析和理解。 当调用new HashMap()和new HashMap(CAP)时,分别执行了不同的构造函数,而二者的构造函数的逻辑是有差异的,当指定容量时,执行了容量参数检查的代码:


public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

  不指定容量时,构造方法内只有一行this.loadFactor = DEFAULT_LOAD_FACTOR;,在put的数据量一致时,后续所有的代码执行流程都是一致的,所以指定容量时,上面容量参数检查的代码带来了额外的性能负担,所以导致数据量较小时指定容量时反而性能更差一些。


 最后回到文章标题上来,Java中使用HashMap时指定初始化容量性能一定会更好嘛?答案是不一定,指定容量也有可能性能会更差。当然,绝大多数情况下还是建议指定容量的,类似的还有ArrayList,也建议指定容量。 别人给出的结论不一定的完全正确的,只有知道产生结论的原因,才能更有效的利用这个结论。

目录
相关文章
|
1月前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
Java之HashMap详解
|
1月前
|
XML Java 数据库连接
性能提升秘籍:如何高效使用Java连接池管理数据库连接
在Java应用中,数据库连接管理至关重要。随着访问量增加,频繁创建和关闭连接会影响性能。为此,Java连接池技术应运而生,如HikariCP。本文通过代码示例介绍如何引入HikariCP依赖、配置连接池参数及使用连接池高效管理数据库连接,提升系统性能。
59 5
|
1月前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
31 4
|
1月前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
39 1
|
1月前
|
Java
Java 静态变量的初始化顺序
【10月更文挑战第15天】了解 Java 静态变量的初始化顺序对于正确编写和维护代码至关重要。通过深入理解初始化顺序的原理和细节,我们可以更好地避免潜在的问题,并提高代码的质量和可靠性。
|
2月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
67 5
|
2月前
|
存储 缓存 算法
提高 Java 数组性能的方法
【10月更文挑战第19天】深入探讨了提高 Java 数组性能的多种方法。通过合理运用这些策略,我们可以在处理数组时获得更好的性能表现,提升程序的运行效率。
40 2
|
2月前
|
存储 缓存 安全
在Java的Map家族中,HashMap和TreeMap各具特色
【10月更文挑战第19天】在Java的Map家族中,HashMap和TreeMap各具特色。HashMap基于哈希表实现,提供O(1)时间复杂度的高效操作,适合性能要求高的场景;TreeMap基于红黑树,提供O(log n)时间复杂度的有序操作,适合需要排序和范围查询的场景。两者在不同需求下各有优势,选择时需根据具体应用场景权衡。
37 2
|
8天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
38 6