1. HashMap 在JDK 7 与 JDK8 下的差别

顺便理一下HashMap.get(Object key)的几个关键步骤,作为后面讨论的基础。

1.1 获取key的HashCode并二次加工

因为对原Key的hashCode质量没信心,怕会存在大量冲突,HashMap进行了二次加工。

JDK7的做法:

h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);

JDK8 因为对自己改造过的哈希大量冲突时的红黑树有信心,所以简单一些,只是把高16位异或下来。

return h ^ (h >>> 16);

所以即使Key比较均匀无哈希冲突,JDK8也比JDK7略快的原因大概于此。

顺便科普一下,Integer的HashCode就是自己,Long要把高32位异或下来变成int, String则是循环累计结果*31+下一个字符,不过因为String是不可变对象,所以生成完一次就会自己cache起来。

1.2 落桶

index = hash & (array.length-1);

桶数组大小是2的指数的好处,通过一次&就够了,而不是代价稍大的取模。

1.3 最后选择Entry

判断Entry是否符合,都是首先哈希值要相等,但因为哈希值不是唯一的,所以还要对比key是否相等,最好是同一个对象,能用==对比,否则要走equals()。 比如String,如果不是同一个对象,equals()起来要一个个字符做比较也是挺累的。

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;

更累的是存在哈希冲突的情况,比如两个哈希值取模后落在同一个桶上,或者两条不同的key有相同的哈希值。
JDK7的做法是建一条链表,后插入的元素在上面,一个个地执行上面的判断。
而JDK8则在链表长度达到8,而且桶数量达到64时,建一棵红黑树,解决严重冲突时的性能问题。

2. 很多人忽视的加载因子Load Factor

加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

2.1 考虑加载因子地设定初始大小

相比扩容时只是System.arraycopy()的ArrayList,HashMap扩容的代价其实蛮大的,首先,要生成一个新的桶数组,然后要把所有元素都重新Hash落桶一次,几乎等于重新执行了一次所有元素的put。

所以如果你心目中有明确的Map 大小,设定时一定要考虑加载因子的存在。

Map map = new HashMap(srcMap.size())这样的写法肯定是不对的,有25%的可能会遇上扩容。

Thrift里的做法比较粗暴, Map map = new HashMap( 2* srcMap.size()), 直接两倍又有点浪费空间。

Guava的做法则是加上如下计算

(int) ((float) expectedSize / 0.75F + 1.0F);

2.2 减小加载因子

在构造函数里,设定加载因子是0.5甚至0.25。
如果你的Map是一个长期存在而不是每次动态生成的,而里面的key又是没法预估的,那可以适当加大初始大小,同时减少加载因子,降低冲突的机率。毕竟如果是长期存在的map,浪费点数组大小不算啥,降低冲突概率,减少比较的次数更重要。

3. Key的设计

对于String型的Key,如果无法保证无冲突而且能用==来对比,那就尽量搞短点,否则一个个字符的equals还是花时间的。

甚至,对于已知的预定义Key,可以自己试着放一下,看冲不冲突。比如,像”a1”,”a2”,”a3” 这种,hashCode是个小数字递增,绝对是不冲突的:)

4. EnumMap

对于上面的问题,有些同学可能会很冲动的想,这么麻烦,我还是换回用数组,然后用常量来定义一些下标算了。其实不用自己来,EnumMap就是可读性与性能俱佳的实现。

EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组,不就是你刚才想做的事情么?

美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断。在JMC里录得不低的采样命中频率。
所以也可以自己再port一个类出来,不实现Map接口,或者自己增加fastGet(),fastPut()的函数。

5. IntObjectHashMap

Netty以及其他FastUtils之类的原始类型map,都支持key是int或 long。但两者的区别并不仅仅在于int 换 Integer的那点空间,而是整个存储结构和Hash冲突的解决方法都不一样。

HashMap的结构是 Node[] table; Node 下面有Hash,Key,Value,Next四个属性。
而IntObjectHashMap的结构是int[] keys 和 Object[] values.

在插入时,同样把int先取模落桶,如果遇到冲突,则不采样HashMap的链地址法,而是用开放地址法(线性探测法)index+1找下一个空桶,最后在keys[index],values[index]中分别记录。在查找时也是先落桶,然后在key[index++]中逐个比较key。

所以,对比整个数据结构,省的不止是int vs Integer,还有每个Node的内容。
而性能嘛,IntObjectHashMap还是稳赢一点的,随便测了几种场景,耗时至少都有24ms vs 28ms的样子,好的时候甚至快1/3。

优化建议

    1. 考虑加载因子地设定初始大小
    2. 减小加载因子
    3. String类型的key,不能用==判断或者可能有哈希冲突时,尽量减少长度
    4. 使用定制版的EnumMap
    5. 使用IntObjectHashMap

高性能场景下,HashMap的优化使用建议的更多相关文章

  1. StringBuilder在高性能场景下的正确用法

    转载:<StringBuilder在高性能场景下的正确用法> by 江南白衣 关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不 ...

  2. [转帖]etcd 在超大规模数据场景下的性能优化

    etcd 在超大规模数据场景下的性能优化   阿里系统软件技术 2019-05-27 09:13:17 本文共5419个字,预计阅读需要14分钟. http://www.itpub.net/2019/ ...

  3. 并发场景下HashMap死循环导致CPU100%的问题

    参考链接:并发场景下HashMap死循环导致CPU100%的问题

  4. etcd 在超大规模数据场景下的性能优化

    作者 | 阿里云智能事业部高级开发工程师 陈星宇(宇慕) 概述 etcd是一个开源的分布式的kv存储系统, 最近刚被cncf列为沙箱孵化项目.etcd的应用场景很广,很多地方都用到了它,例如kuber ...

  5. 高并发场景下的httpClient优化使用

    1.背景 我们有个业务,会调用其他部门提供的一个基于http的服务,日调用量在千万级别.使用了httpclient来完成业务.之前因为qps上不去,就看了一下业务代码,并做了一些优化,记录在这里. 先 ...

  6. 特定场景下SQL的优化

    1.大表的数据修改最好分批处理. 1000万行的记录表中删除更新100万行记录,一次只删除或更新5000行数据.每批处理完成后,暂停几秒中,进行同步处理. 2.如何修改大表的表结构. 对表的列的字段类 ...

  7. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  8. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  9. HttpClient在高并发场景下的优化实战

    在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...

随机推荐

  1. 查看容器IP地址

    我们可以通过以下命令查看容器运行的各种数据 docker inspect 容器名称(容器ID)  也可以直接执行下面的命令直接输出IP地址 docker inspect --format='{{.Ne ...

  2. .NET 4.5 中新提供的压缩类(转载)

    Windows8 的开发已经如火如荼开始了,在 Windows8 中提供的 .NET Framework 已经更新到了 4.5 版,其中又增加了一些新的特性,对压缩文件的支持就是其中之一. 在 4.5 ...

  3. Scrapy框架的学习(6.item介绍以及items的使用(提前定义好字段名))转载https://blog.csdn.net/wei18791957243/article/details/86259688

      在Scrapy框架中的items.py的作用   1.可以预先定义好要爬取的字段     items.py import scrapy     class TencentItem(scrapy.I ...

  4. elasticsearch 安装 windows linux macOS

    导读 在上一章节我们介绍Elasticsearch基本概念,今天我们继续进行本章内容,Elasticsearch在各种环境下安装,下面将逐一讲解在各种操作系统或不同安装在不同环境中注意事项. 安装 E ...

  5. Python在金融量开源项目列表

    Python也已经在金融量化投资领域占据了重要位置,开源项目列表:

  6. Twisted简介

    Twisted是用Python实现的基于事件驱动的网络引擎框架,Twisted支持许多常见的传输及应用层协议,包括TCP.UDP.SSL/TLS.HTTP.IMAP.SSH.IRC以及FTP.就像Py ...

  7. 生成id

    package com.develop.web.util; import java.security.MessageDigest; import java.text.SimpleDateFormat; ...

  8. Docker Kubernetes 容器扩容与缩容

    Docker Kubernetes 容器扩容与缩容 环境: 系统:Centos 7.4 x64 Docker版本:18.09.0 Kubernetes版本:v1.8 管理节点:192.168.1.79 ...

  9. springboot shiro和freemarker集成之权限控制完全参考手册(跳过认证,登录由三方验证,全网首发)

    本文主要考虑单点登录场景,登录由其他系统负责,业务子系统只使用shiro进行菜单和功能权限校验,登录信息通过token从redis取得,这样登录验证和授权就相互解耦了. 用户.角色.权限进行集中式管理 ...

  10. ORA-55617解决方法

    昨天一测试环境出现异常ORA-55617: Flashback Archive "XXXXX" runs out of space and tracking on "XX ...