zoukankan      html  css  js  c++  java
  • 高性能场景下,HashMap的优化使用建议

    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
  • 相关阅读:
    HDU 2433 Travel (最短路,BFS,变形)
    HDU 2544 最短路 (最短路,spfa)
    HDU 2063 过山车 (最大匹配,匈牙利算法)
    HDU 1150 Machine Schedule (最小覆盖,匈牙利算法)
    290 Word Pattern 单词模式
    289 Game of Life 生命的游戏
    287 Find the Duplicate Number 寻找重复数
    283 Move Zeroes 移动零
    282 Expression Add Operators 给表达式添加运算符
    279 Perfect Squares 完美平方数
  • 原文地址:https://www.cnblogs.com/Pjson/p/8919439.html
Copyright © 2011-2022 走看看