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. HDU 2586 How far away(dfs+邻接表)

    How far away [题目链接]How far away [题目类型]dfs+邻接表 &题意: 题目大意:一个村子里有n个房子,这n个房子用n-1条路连接起来,接下了有m次询问,每次询问 ...

  2. python assert断言函数

    python assert断言是声明布尔值必须为真的判定,如果发生异常就说明表达式为假. 可以理解assert断言语句为raise-if-not,用来测试表示式,其返回值为假,就会触发异常. self ...

  3. python split()函数的用法

    转自: https://blog.csdn.net/orangefly0214/article/details/80810449 函数:split() Python中有split()和os.path. ...

  4. jqueryd的post传递表单以及取消表单的默认传递

    //取消表单的默认传递: <form method="post" onsubmit="return false;"> 在FORM属性里添加 onsu ...

  5. photoshop cc 安装失败 2%

    photoshop cc 安装失败 2%   C盘--Program Files---Common Files--Adobe--caps ,把这个文件夹中的文件全部删除,然后再安装     C:\Pr ...

  6. java41 类的高级概念

  7. JavaScript Dom 绑定事件

    JavaScript  Dom 绑定事件 // 先获取Dom对象,然后进行绑定 document.getElementById('xx').onclick document.getElementByI ...

  8. Python socket文件传送md5校验

    soket_server import socket,os,hashlib server = socket.socket() server.bind(('0.0.0.0',9999)) server. ...

  9. 剑指offer(36)两个链表中的第一个公共节点

    题目描述 输入两个链表,找出它们的第一个公共结点. 题目分析 我发现关于链表的题都涉及双指针,大家做的时候记得用双指针. 题目理解了就很好做了,比较简单,先在长的链表上跑,知道长的和短的一样长,再一起 ...

  10. 遗留系统如何用DDD重构(未完,待续)

    GETTING DDD STARTED SURROUNDED BY LEGACY SYSTEMS ByEric Evans