重写hashcode,玩儿坏HashMap

简介: 重写hashcode,玩儿坏HashMap ##缘起 提到HashMap,都知道“线程不安全,并发情况下不能用”,除此之外貌似了解的不是很多。 对,我就是其中一员。 这两天跟两个开发大神学习了一下,并参考了网上的一些文档,浅显整理如下。 ##关键点 1.HashMap的定位是单线程高性能的 key-value 存储数据结构,多线程请使用ConcurrentHashMap

重写hashcode,玩儿坏HashMap

缘起

提到HashMap,都知道“线程不安全,并发情况下不能用”,除此之外貌似了解的不是很多。
对,我就是其中一员。

这两天跟两个开发大神学习了一下,并参考了网上的一些文档,浅显整理如下。

关键点

1.HashMap的定位是单线程高性能的 key-value 存储数据结构,多线程请使用ConcurrentHashMap。
2.HashMap性能好坏的关键是避免hash冲突,以及冲突很多的情况下,如何更高效。

前提:完美Hash函数不存在

如果让每个key的hashcode都不一样,那无疑是最高效的,但实际的情况是不可能的,因为你没有无限的内存,这种假设不成立。
所以,Java中实现HashMap的hashcode返回值是整形,根据鸽笼理论,当我们的对象超过整形最大值时,这些对象会发生哈希碰撞。

实现:分桶

除了上面完美Hash函数不存在,通过允许合理的哈希碰撞来节省内存,也是HashMap的考虑,而且很多情况下,Hash值经常集中在几个特定的值上
所以,看HashMap源码:

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认是分了M=16个桶来进行分桶存储。所以,冲突的概率是1/M。

冲突后的实现:分离链表

下图是HashMap的数据结构图,前面是桶,后面是每个桶的链表结构。

当桶没有出现hash冲突时,hashmap查找元素是很快的。但桶hash冲突后,每个桶就变成了单的链表。
这种情况只能顺序遍历每个节点,直到找到想搜索的节点为止,极端情况,会达到最坏时间复杂度。(见下面:java8优化点)

但这种实现,至少比另一种实现方式:开放寻址的方法要好,理由如下:(摘自网络)

开放寻址使用连续的空间,因此有着缓存效率的提升。因此当数据量较小时,能够放到系统缓存中时,开放寻址会表现出比分离链接更好的性能。
但是当数据量增长时,它的性能就会越差,因为我们无法期待一个大的数组能够得到缓存性能优化。
这也是HashMap使用分离链表来解决哈希冲突的原因。此外,开放寻址还有一个弱点。我们调用remove()方法会十分频繁,当我们删除数据时,一个特定的哈希冲突,可能会干扰总体的性能,而分离链表则没有这样的缺点。”

java8优化分离链表:引入红黑树

除了最坏复杂度之外,如果数据很多,hash值不平均,比如集中在某几个特定的值上,使用树会比链表有着更快的性能。
所以,java8里面使用如下策略进行链表or树结构的转换。即:当冲突的元素数=8时,链表变为树;当减少到6时,树切换为链表。

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

补充说明:(摘自网络)

另外,java8里面,HashMap使用Node类替代了Entry类,它们的结构大体相同。
但显著地差别是,Node类具有导出类TreeNode,通过这种继承关系,一个链表很容易被转换成树。

冲突还是要解决,动态扩容桶

哈希桶的默认数量是16,最大值是整形最大值。如果桶数量不变,即便上述内容,性能也不会明显的提升,
所以,HashMap使用下面方式进行动态扩容。

当桶数量超过当前值 * DEFAULT_LOAD_FACTOR 时,就自动扩容,然后重新插入所有数据。
例如当桶数到达: 初始桶数16*阀值0.75 = 12时,自动扩容到 16*2 = 32个桶。这样,冲突就立减降低,但重新插入性能也有损耗。
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

通过重写hashcode,论证上述内容

测试类如下:

package com.taobao.huachuan;

import java.util.HashMap;
import java.util.Map;

/**
 * 对比hash冲突和不冲突
 * @author sheshijie
 *
 */
public class TestHashMap {

    public static void main(String[] args) {
        
        Map<TaoPP1, Integer> m1 =new HashMap<TaoPP1, Integer>();
        Long start1 = System.currentTimeMillis();
        for(int i = 0 ;i<10000;i++)
        {
            TaoPP1 tp1 = new TaoPP1();
            tp1.setAge(i);
            tp1.setValue(i);
            
            m1.put(tp1, i);
            
        }
        System.out.println("hashcode不冲突的耗时:"+(System.currentTimeMillis()-start1) +" size:" + m1.size());
        
        
        Map<TaoPP2, Integer> m2 =new HashMap<TaoPP2, Integer>();
        Long start2 = System.currentTimeMillis();
        for(int i = 0 ;i<10000;i++)
        {
            TaoPP2 tp2 = new TaoPP2();
            tp2.setAge(i);
            tp2.setValue(i);
            
            m2.put(tp2, i);
            
        }
        System.out.println("hashcode冲突的耗时:"+(System.currentTimeMillis()-start2) +" size:" + m2.size());
                
        Long start3 = System.currentTimeMillis();
        System.out.println("不冲突寻址 "+m1.get(new TaoPP2())+"耗时:"+(System.currentTimeMillis()-start3));
        
        Long start4 = System.currentTimeMillis();
        System.out.println("冲突寻址 耗时:"+m2.get(new TaoPP2())+"耗时:"+(System.currentTimeMillis()-start4));
    }

}

重写hashcode的方法如下:

    /**
     * 大量hashCode重复
     */
     @Override
        public int hashCode() {
            return (age+value)%16;//反复调整,可以看出差距明显
        }

实际的测试结果为:

hashcode不冲突的耗时:16 size:10000
hashcode冲突的耗时:867 size:10000

果然,差距真的好大。
调整hashCode方法 return (age+value)%1024;
测试结果为:

hashcode不冲突的耗时:31 size:10000
hashcode冲突的耗时:78 size:10000

浅显的证明了hashcode冲突的多少,是性能的关键。

而冲突后查找性能更加缓慢,结果如下:

不冲突寻址 null耗时:0
冲突寻址 耗时:null耗时:9

我是测试,写错了不要喷,要爱护我

我是测试,写错了不要喷,要爱护我

我是测试,写错了不要喷,要爱护我

重要的事情说三遍!

相关文章
|
8天前
|
Java
JAVA中比较对象是否相等的方式是什么?为什么重写equals就一定要重写hashcode?百天百题(3/100)
JAVA中比较对象是否相等的方式是什么?为什么重写equals就一定要重写hashcode?
|
8天前
|
安全
搞懂HashTable, HashMap, ConcurrentHashMap 的区别,看着一篇就足够了!!!
搞懂HashTable, HashMap, ConcurrentHashMap 的区别,看着一篇就足够了!!!
41 0
|
5月前
|
存储 算法 Java
史上最全的Java容器集合之equals 和 hashCode
史上最全的Java容器集合之equals 和 hashCode
41 0
|
存储 设计模式 Java
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(上)
|
算法 Java
【HashMap】由tableSizeFor想到的一个算法
【HashMap】由tableSizeFor想到的一个算法
111 0
|
存储 Java 程序员
深究HashSet
HashSet底层原理梳理总结
105 0
|
存储 Java iOS开发
深究equals
在我们做项目的时候经常会将两个对象或者变量作比较,这时候会用到equals或者==,二者的区别在我们初学Java时就已经搞清楚了,一个比较的是值是否相等,另一个比较的是地址是否相等,但是equals真的只是比较两个值是否相等吗?如果是,那应该如何比较呢?
95 0
深究equals
HashMap 使用的时候指定容量?你真的用明白了吗?(值得一阅)
HashMap 使用的时候指定容量?你真的用明白了吗?(值得一阅)
245 0
HashMap 使用的时候指定容量?你真的用明白了吗?(值得一阅)
|
存储 Java
三句话都能解释清楚的java集合类HashSet,你清楚吗?
HashSet作为一种最简单的java集合类,真的可以用三句话来概括一下: 第一句:存放不重复的数据。 第二句:底层基于hash表实现。 第三句:内部基于HashMap。 这也就是说,你想要完完全全彻彻底底地把HashSet吃透,就一定要先吃透HashMap。这篇文章将带着你从特点到存储,再到最后的实现,从源码角度来分析一下。
103 0
|
存储 Java
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(下)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(下)
ThreadLocal的短板,我 TransmittableThreadLocal 来补上!(下)