0.前言

本文主要讨论哈希冲突下的一些性能测试。

为什么要写这篇文章,不是为了KPI不是为了水字数。

hashmap是广大JAVA程序员最为耳熟能详,使用最广泛的集合框架。它是大厂面试必问,著名八股经必备。在小公司呢?这些年也面过不少人,对于3,5年以上的程序员,问到hashmap也仅限于要求知道底层是数组+链表,知道怎么放进去,知道有哈希冲突这么一回事即可,可依然免不了装备的嫌疑。

可hashmap背后的思想,在缓存,在数据倾斜,在负载均衡等分布式大数据领域都能广泛看到其身影。了解其背后的思想不仅仅只是为了一个hashmap.

更重要的是,hashmap不像jvm底层原理那么遥远,不像并发编程那么宏大,它只需要通勤路上十分钟就可搞定基本原理,有什么理由不呢?

所以本文试着从相对少见的一个微小角度来重新审视一下hashmap.

1.准备工作。

1.1模拟哈希冲突

新建两个class,一个正常重写equalshashcode方法,一个故意在hashcode方法里返回一定范围内的随机数,模拟哈希冲突,以及控制哈希冲突的程序。

不冲突的类

  1. @Setter
  2. public class KeyTest2 {
  3. private String name;
  4. @Override
  5. public boolean equals(Object o) {
  6. if (this == o) return true;
  7. if (o == null || getClass() != o.getClass()) return false;
  8. KeyTest2 keyTest = (KeyTest2) o;
  9. return name != null ? name.equals(keyTest.name) : keyTest.name == null;
  10. }
  11. @Override
  12. public int hashCode() {
  13. return name != null ? name.hashCode() : 0;
  14. }
  15. }

冲突的类

  1. @Setter
  2. @NoArgsConstructor
  3. public class KeyTest {
  4. private String name;
  5. private Random random;
  6. public KeyTest(Random random){
  7. this.random = random;
  8. }
  9. @Override
  10. public boolean equals(Object o) {
  11. if (this == o) return true;
  12. if (o == null || getClass() != o.getClass()) return false;
  13. KeyTest keyTest = (KeyTest) o;
  14. return name != null ? name.equals(keyTest.name) : keyTest.name == null;
  15. }
  16. @Override
  17. public int hashCode() {
  18. // return name != null ? name.hashCode() : 0;
  19. return random.nextInt(1000);
  20. }
  21. }

众所周知,hashmap在做put的时候,先根据key求hashcode,找到数组下标位置,如果该位置有元素,再比较equals,如果返回true,则替换该元素并返回被替换的元素;否则就是哈希冲突了,即hashcode相同但equals返回false。

哈希冲突的时候在冲突的数组处形成数组,长度达到8以后变成红黑树。

1.2 java的基准测试。

这里使用JMH进行基准测试.

JMH是Java Microbenchmark Harness的简称,一般用于代码的性能调优,精度甚至可以达到纳秒级别,适用于 java 以及其他基于 JVM 的语言。和 Apache JMeter 不同,JMH 测试的对象可以是任一方法,颗粒度更小,而不仅限于rest api.

jdk9以上的版本自带了JMH,如果是jdk8可以使用maven引入依赖。

点击查看JMH依赖
  1. <dependency>
  2. <groupId>org.openjdk.jmh</groupId>
  3. <artifactId>jmh-core</artifactId>
  4. <version>${jmh.version}</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.openjdk.jmh</groupId>
  8. <artifactId>jmh-generator-annprocess</artifactId>
  9. <version>${jmh.version}</version>
  10. </dependency>

2.测试初始化长度

点击查看初始化长度基本测试代码
  1. /使用模式 默认是Mode.Throughput
  2. @BenchmarkMode(Mode.AverageTime)
  3. // 配置预热次数,默认是每次运行1秒,运行10次,这里设置为3次
  4. @Warmup(iterations = 3, time = 1)
  5. // 本例是一次运行4秒,总共运行3次,在性能对比时候,采用默认1秒即可
  6. @Measurement(iterations = 3, time = 4)
  7. // 配置同时起多少个线程执行
  8. @Threads(1)
  9. //代表启动多个单独的进程分别测试每个方法,这里指定为每个方法启动一个进程
  10. @Fork(1)
  11. // 定义类实例的生命周期,Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
  12. @State(value = Scope.Benchmark)
  13. // 统计结果的时间单元
  14. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  15. public class HashMapPutResizeBenchmark {
  16. @Param(value = {"1000000"})
  17. int value;
  18. /**
  19. * 初始化长度
  20. */
  21. @Benchmark
  22. public void testInitLen(){
  23. HashMap map = new HashMap(1000000);
  24. Random random = new Random();
  25. for (int i = 0; i < value; i++) {
  26. KeyTestConflict test = new KeyTestConflict(random, 10000);
  27. test.setName(i+"");
  28. map.put(test, test);
  29. }
  30. }
  31. /**
  32. * 不初始化长度
  33. */
  34. @Benchmark
  35. public void testNoInitLen(){
  36. HashMap map = new HashMap();
  37. for (int i = 0; i < value; i++) {
  38. Random random = new Random();
  39. KeyTestConflict test = new KeyTestConflict(random, 10000);
  40. test.setName(i+"");
  41. map.put(test, test);
  42. }
  43. }
  44. public static void main(String[] args) throws RunnerException {
  45. Options opt = new OptionsBuilder()
  46. .include(HashMapPutResizeBenchmark.class.getSimpleName())
  47. .mode(Mode.All)
  48. // 指定结果以json结尾,生成后复制可去:http://deepoove.com/jmh-visual-chart/ 或https://jmh.morethan.io/ 得到可视化界面
  49. .result("hashmap_result_put_resize.json")
  50. .resultFormat(ResultFormatType.JSON).build();
  51. new Runner(opt).run();
  52. }
  53. }

测试结果图

对测试结果图例做一个简单的说明:

以上基准测试,会得到一个json格式的结果。然后将该结果上传到官方网站,会得到一个上述图片的结果。

横坐标,红色驻图代表有冲突,浅蓝色驻图无冲突。

众坐标,ops/ns代表平均每次操作花费的时间,单位为纳秒,1秒=1000000000纳秒,这样更精准。

下同。

简单说,驻图越高代表性能越低。

我测了两次,分别是无哈希冲突和有哈希冲突的,这里只贴一种结果。

测试结果表明,hashmap定义时有初始化对比无初始化,有大约4%到12%的性能损耗。

足够的初始化长度下,有哈希冲突的测试结果:

足够的初始化长度下,没有哈希冲突的测试结果:

3.模拟一百万个元素put,get的差异。

众所周知,hashmap在频繁做resize时,性能损耗非常严重。以上是没初始化长度,无冲突和有冲突的情况下,前者性能是后者性能的53倍。

那么在初始化长度的情况下呢?

  1. HashMap map = new HashMap(1000000);

同样的代码下,得到的测试结果



以上是有初始化长度,无冲突和有冲突的情况下,前者性能是后者性能的58倍。

大差不差,不管有无初始化长度,无冲突的效率都是有冲突效率的50倍以上。说明,这是哈希冲突带来的性能损耗。

4.模拟无红黑树情况下get效率

4.1 将random扩大,哈希冲突严重性大大减小,模拟大多数哈希冲突导致的哈希链长度均小于8,无法扩展为红黑树,只能遍历数组。

将KeyTest的hashcode方法改为:

  1. @Override
  2. public int hashCode() {
  3. // return name != null ? name.hashCode() : 0;
  4. return random.nextInt(130000);
  5. }

这样1000000/130000 < 8,这样大多数的哈希链将不会扩展为红黑树。

测试结果为:

测试结果说明,**有冲突的效率反而比无冲突的效率要高**,差不多高出80%左右。

这其实有点违反常识,我们通常讲,hashmap要尽量避免哈希冲突,哈希冲突的情况下写入和读取性能都会受到很大的影响。

但是上面的测试结果表明,大数据量相对比较大的时候,适当的哈希冲突(<8)反而读取效率更高。

个人猜测是,适当的哈希冲突,数组长度大为减少。

为了证明以上猜想,直接对ArrayList进行基准测试。

4.1.1 ArrayList不同长度下get效率的基准测试

模拟一个哈希冲突非常严重下,底层数组长度较小的list,和哈希冲突不严重情况下,底层数组较大的list,再随机测试Get的效率如何。

点击查看测试代码
  1. //使用模式 默认是Mode.Throughput
  2. @BenchmarkMode(Mode.AverageTime)
  3. // 配置预热次数,默认是每次运行1秒,运行10次,这里设置为3次
  4. @Warmup(iterations = 3, time = 1)
  5. // 本例是一次运行4秒,总共运行3次,在性能对比时候,采用默认1秒即可
  6. @Measurement(iterations = 3, time = 4)
  7. // 配置同时起多少个线程执行
  8. @Threads(1)
  9. //代表启动多个单独的进程分别测试每个方法,这里指定为每个方法启动一个进程
  10. @Fork(1)
  11. // 定义类实例的生命周期,Scope.Benchmark:所有测试线程共享一个实例,用于测试有状态实例在多线程共享下的性能
  12. @State(value = Scope.Benchmark)
  13. // 统计结果的时间单元
  14. @OutputTimeUnit(TimeUnit.NANOSECONDS)
  15. public class ArrayListGetBenchmark {
  16. // @Param(value = {"1000","100000","1000000"})
  17. @Param(value = {"1000000"})
  18. int value;
  19. @Benchmark
  20. public void testConflict(){
  21. int len = 10000;
  22. Random random = new Random(len);
  23. for (int i = 0; i < 100; i++) {
  24. int index = random.nextInt(len);
  25. System.out.println("有冲突,index = " + index);
  26. ConflictHashMapOfList.list.get(index);
  27. }
  28. }
  29. @Benchmark
  30. public void testNoConflict(){
  31. int len = 1000000;
  32. Random random = new Random(len);
  33. for (int i = 0; i < 100; i++) {
  34. int index = random.nextInt(len);
  35. System.out.println("无冲突,index = " + index);
  36. NoConflictHashMapOfList.list.get(index);
  37. }
  38. }
  39. public static void main(String[] args) throws RunnerException {
  40. Options opt = new OptionsBuilder()
  41. .include(HashMapGetBenchmark.class.getSimpleName())
  42. .mode(Mode.All)
  43. // 指定结果以json结尾,生成后复制可去:http://deepoove.com/jmh-visual-chart/ 或https://jmh.morethan.io/ 得到可视化界面
  44. .result("arraylist_result_get_all.json")
  45. .resultFormat(ResultFormatType.JSON).build();
  46. new Runner(opt).run();
  47. }
  48. @State(Scope.Thread)
  49. public static class ConflictHashMapOfList {
  50. volatile static ArrayList list = new ArrayList();
  51. static int randomMax = 10000;
  52. static {
  53. // 模拟哈希冲突严重,数组长度较小
  54. for (int i = 0; i < randomMax; i++) {
  55. list.add(i);
  56. }
  57. }
  58. }
  59. @State(Scope.Thread)
  60. public static class NoConflictHashMapOfList {
  61. volatile static ArrayList list = new ArrayList();
  62. static int randomMax = 1000000;
  63. static {
  64. // 模拟没有哈希冲突,数组长度较大
  65. for (int i = 0; i < randomMax; i++) {
  66. list.add(i);
  67. }
  68. }
  69. }
  70. }

测试结果如下:



可以看到,间接证实了以上的猜想。

当然这里的代码可能并不严谨,也欢迎大家一起讨论。

4.2 jdk1.8版本,哈希冲突严重下的get效率测试



测试结果说明:在jdk8,无冲突效率是有有冲突的3倍左右。

4.3 将jdk版本降为1.7,在哈希冲突依然严重的情况下,get效率如何?



测试结果说明:在jdk7,无冲突效率是有有冲突的12倍左右。

结合4.1和4.2的测试对比,说明jdk1.8红黑树的优化效率确实提升很大。

5.总结

1.初始化的时候指定长度,长度要考虑到负载因子0.75.初始化的影响受到哈希冲突的影响,没有那么大(相对于倍数而言),但也不小。

2.哈希冲突严重时,put性能急剧下降。(几十倍级)

3.相同元素个数的前提下,在哈希冲突时,get效率反而更高。

4.相比之前的版本,哈希冲突严重时,jdk8红黑树对get效率有非常大的提升。

测试代码和测试结果在 这里

hashmap的一些性能测试的更多相关文章

  1. 数据结构HashMap(Android SparseArray 和ArrayMap)

    HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...

  2. HashMap和布隆过滤器命中性能测试

    package datafilter; import com.google.common.base.Stopwatch; import com.google.common.hash.BloomFilt ...

  3. java——HashMap的实现原理,自己实现简单的HashMap

    数据结构中有数组和链表来实现对数据的存储,但是数组存储区间是连续的,寻址容易,插入和删除困难:而链表的空间是离散的,因此寻址困难,插入和删除容易. 因此,综合了二者的优势,我们可以设计一种数据结构-- ...

  4. HashMap循环遍历方式及其性能对比(zhuan)

    http://www.trinea.cn/android/hashmap-loop-performance/ ********************************************* ...

  5. [Java] 多个Map的性能比较(TreeMap、HashMap、ConcurrentSkipListMap)

    比较Java原生的 3种Map的效率. 1.  TreeMap 2.  HashMap 3.  ConcurrentSkipListMap 结果: 模拟150W以内海量数据的插入和查找,通过增加和查找 ...

  6. 介绍4款json的java类库 及 其性能测试

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式. 易于人阅读和编写.同时也易于机器解析和生成. 它基于JavaScript Programming Lan ...

  7. HashMap循环遍历方式及其性能对比

    主要介绍HashMap的四种循环遍历方式,各种方式的性能测试对比,根据HashMap的源码实现分析性能结果,总结结论.   1. Map的四种遍历方式 下面只是简单介绍各种遍历示例(以HashMap为 ...

  8. Java HashMap工作原理及实现

    Java HashMap工作原理及实现 2016/03/20 | 分类: 基础技术 | 0 条评论 | 标签: HASHMAP 分享到:3 原文出处: Yikun 1. 概述 从本文你可以学习到: 什 ...

  9. 【hashMap】详谈

    官方文档地说明 几个关键的信息:基于Map接口实现.允许null键/值.非同步.不保证有序(比如插入的顺序).也不保证序不随时间变化. 一.概述 HashMap 是一个散列表,它存储的内容是键值对(k ...

  10. 数据库之redis篇(2)—— redis配置文件,常用命令,性能测试工具

    redis配置 如果你是找网上的其他教程来完成以上操作的话,相信你见过有的启动命令是这样的: 启动命令带了这个参数:redis.windows.conf,由于我测试环境是windows平台,所以是这个 ...

随机推荐

  1. 支持JDK19虚拟线程的web框架,之二:完整开发一个支持虚拟线程的quarkus应用

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本篇是<支持JDK19虚拟线程的web ...

  2. ysoserial CommonsCollections2 分析

    在最后一步的实现上,cc2和cc3一样,最终都是通过TemplatesImpl恶意字节码文件动态加载方式实现反序列化. 已知的TemplatesImpl->newTransformer()是最终 ...

  3. IO学习笔记

    IO File 概述 构造方法 代码实现: public class FileDemo001 { public static void main(String[] args) { File f1 = ...

  4. C#使用GDI+同时绘制图像和ROI在picturebox上

    Bitmap bmp; /// <summary> /// 绘制图像 /// </summary> /// <param name="g">Gr ...

  5. CB利用链及无依赖打Shiro

    前言 前面已经学习了CC1到CC7的利用链,其中在CC2中认识了java.util.PriorityQueue ,它在Java中是一个优先队列,队列中每一个元素有自己的优先级.在反序列化这个对象时,为 ...

  6. 2022-11-12 Acwing每日一题

    本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...

  7. mysql不需要密码,乱输入密码就能进去。。。。解决

    为什么MySQL 不用输入用户名和密码也能访问 今天后天连接数据库时密码写错了,却发现后台能够拿到数据库中的数据,又故意把用户名和密码都写错,结果还是可以.这就意味着任何一个人只要登入服务器,就可以轻 ...

  8. 使用echarts(可视化图表库)

    一:echarts 1.简介 一个基于 JavaScript 的开源可视化图表库 echarts官网使用教程: https://echarts.apache.org/zh/index.html 2.e ...

  9. overflow:scroll修改样式

    当overflow :scroll 出现滚动条后,默认的滚动条样式太丑了,不是我们想要的,那么我们来修改一下吧!~ 话不多说,直接上代码  /* 定义滚动条样式 */ ::-webkit-scroll ...

  10. axios 中get 和post传参

    axios中get和ppost传参的方式: params是添加到url的请求字符串中的,一般用于get请求. data是添加到请求体(body)中的, 一般用于post请求. 上面,只是一般情况. 其 ...