• Java性能优化——HashCode的使用


    背景

    告警子系统监控4万个大网元所有端口的某些指标数据,根据阈值配置判断是否产生告警。采集——数据处理子系统每5分钟会主动采集24万次数据,发送24万条消息给告警子系统,这24万条消息涉及100万实体的数十个指标数据。告警子系统采用多节点部署方式分担压力,每个节点处理不同网元类型,不同实体,不同指标的数据。海量数据的过滤,必然会大量使用集合逻辑运算,使用不当,则会造成性能瓶颈。

    例子

    存在告警节点监控的实体动态变化,所以每个告警节点需要动态维护自己的监控列表,所以代码中会用到Collection.removeAll求差集的计算,计算出新增的实体,然后进一步计算出这些新增实体的历史平均值,方差等数据。

    package com.coshaho.hash;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class HashObject {
        
        public static void main(String[] args)
        {
            List<String> list1 = new ArrayList<String>();
            List<String> list2 = new ArrayList<String>();
            
            // 2000长度的List求差集
            for(int i = 0; i < 2000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            long startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            long endTime = System.currentTimeMillis();
            System.out.println("2000 list remove all cost: " + (endTime - startTime) + "ms.");
            
            // 10000长度的List求差集
            list1.clear();
            list2.clear();
            for(int i = 0; i < 10000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            endTime = System.currentTimeMillis();
            System.out.println("10000 list remove all cost: " + (endTime - startTime) + "ms.");
            
            // 50000长度的List求差集
            list1.clear();
            list2.clear();
            for(int i = 0; i < 50000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            endTime = System.currentTimeMillis();
            System.out.println("50000 list remove all cost: " + (endTime - startTime) + "ms.");
        }
    }

    上述代码我们分别对长度为2000,10000,50000的List进行了求差集的运算,耗时如下:

    2000 list remove all cost: 46ms.
    10000 list remove all cost: 1296ms.
    50000 list remove all cost: 31028ms.

    可以看到,数据量每增加5倍,ArrayList的求差集运算时间消耗增加30倍。当我们进行数十万元素的求差集运算时,时间消耗是我们不可承受的。

    Equals

    实体过滤中,为了找到我们关心的实体数据,我们必然会采用Collection.contains过滤实体ID,这里面会使用到字符串equals方法判断两个ID是否相等。对于我们来说,两个字符串相等的含义就是两个字符串长度一致,对应位置的字符编码相等。如果大量字符串两两比较都采用上述算法,那将会进行海量的运算,消耗大量性能。这个时候,HashCode的作用就显得尤其重要。

    HashCode

    HashCode是int类型。两个对象如果相等(equals为true),则HashCode必然相等;反之,HashCode不等的两个对象,equals必然为false。最优秀的Hash算法,不相等的对象HashCode都不相同,所有equals比较都只调用HashCode的恒等比较,那么计算量就大大减小了。实际上,任何一个Hash算法都不能达到上述要求(HashCode为int类型,说明HashCode取值范围有限,对象超过int取值范围个数,就必然出现不相等对象对应同一个HashCode值)。不相等的对象对应相同的HashCode称之为Hash冲突。

    但是,好的Hash算法确出现Hash冲突的概率极低。比如0.01%的Hash冲突概率,这样就意味着,我们平均进行10000次不相等对象的equals比较,只会出现一次Hash冲突,也就意味着只需要调用一次equals主逻辑。我们在设计equals方法时,先比较两个对象HashCode是否相等,不相等则返回false,相等才进行equals主逻辑比较。

    原始的HashCode方法是由虚拟机本地实现的,可能采用的对象地址进行运算。String复写了HashCode方法,代码如下:

        // Object
        public native int hashCode();
    
        // String
        public int hashCode() {
            int h = hash;
            if (h == 0 && value.length > 0) {
                char val[] = value;
    
                for (int i = 0; i < value.length; i++) {
                    h = 31 * h + val[i];
                }
                hash = h;
            }
            return h;
        }

    HashMap
    HashMap是一个利用Key的HashCode进行散列存储的容器。它采用数组->链表->红黑树存储数据。结构如下图:

    最简单的设想,计算一个Key在数组中的位置时,采用HashCode%数组长度求余计算则可(实际上JDK采用了更好的散列算法)。可以想象,相同的散列算法下,数组长度越长,Hash冲突概率越小,但是使用的空间越大。

    JDK默认采用0.75为元素容量与数组长度的比例。默认初始化数组长度为16(采用2的n次方是考虑HashMap的扩容性能),当元素个数增加到16*0.75=12个时,数组长度会自动增加一倍,元素位置会被重新计算。在数据量巨大的情况下,我们初始化HashMap时应该考虑初始化足够的数组长度,特别是性能优先的情况下,我们还可以适当减小元素容量与数组长度的比例。HashMap部分源码:

        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        /**
         * The maximum capacity, used if a higher value is implicitly specified
         * by either of the constructors with arguments.
         * MUST be a power of two <= 1<<30.
         */
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        /**
         * Constructs an empty <tt>HashMap</tt> with the specified initial
         * capacity and load factor.
         *
         * @param  initialCapacity the initial capacity
         * @param  loadFactor      the load factor
         * @throws IllegalArgumentException if the initial capacity is negative
         *         or the load factor is nonpositive
         */
        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
    
            this.loadFactor = loadFactor;
            threshold = initialCapacity;
            init();
        }
    
        /**
         * Constructs an empty <tt>HashMap</tt> with the specified initial
         * capacity and the default load factor (0.75).
         *
         * @param  initialCapacity the initial capacity.
         * @throws IllegalArgumentException if the initial capacity is negative.
         */
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
        /**
         * Constructs an empty <tt>HashMap</tt> with the default initial capacity
         * (16) and the default load factor (0.75).
         */
        public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }

    大数据集合运算性能考虑
    通过上述分析,我们知道在性能优先的场景下,大数据集合运算一定要使用Hash集合(HashMap,HashSet,HashTable)存储数据。文章开头的集合求余运算,我们修改为使用HashSet.removeAll,代码如下:

    package com.coshaho.hash;
    
    import java.util.Collection;
    import java.util.HashSet;
    
    public class HashObject {
        
        public static void main(String[] args)
        {
            Collection<String> list1 = new HashSet<String>();
            Collection<String> list2 = new HashSet<String>();
            
            // 2000长度的List求差集
            for(int i = 0; i < 2000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            long startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            long endTime = System.currentTimeMillis();
            System.out.println("2000 list remove all cost: " + (endTime - startTime) + "ms.");
            
            // 10000长度的List求差集
            list1.clear();
            list2.clear();
            for(int i = 0; i < 10000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            endTime = System.currentTimeMillis();
            System.out.println("10000 list remove all cost: " + (endTime - startTime) + "ms.");
            
            // 50000长度的List求差集
            list1.clear();
            list2.clear();
            for(int i = 0; i < 50000; i++)
            {
                list1.add("" + i);
                list2.add("" + (i + 1));
            }
            startTime = System.currentTimeMillis();
            list1.removeAll(list2);
            endTime = System.currentTimeMillis();
            System.out.println("50000 list remove all cost: " + (endTime - startTime) + "ms.");
        }
    }

    运行效果如下:

    2000 list remove all cost: 31ms.
    10000 list remove all cost: 0ms.
    50000 list remove all cost: 16ms.


     

  • 相关阅读:
    使用Python画ROC曲线以及AUC值
    Machine Learning : Pre-processing features
    资源 | 数十种TensorFlow实现案例汇集:代码+笔记
    在 Mac OS X 终端里使用 Solarized 配色方案
    编译安装GCC 4.7.2
    Office -Word 公式插件Aurora的使用 ——在 Word 中插入 LaTex 公式
    LaTeX 写中文论文而中文显示不出来
    LaTeX 公式编辑之 把符号放在正下方
    Python 判断字符串是否含有指定字符or字符串
    Python 中使用 pandas Dataframe 删除重复的行
  • 原文地址:https://www.cnblogs.com/coshaho/p/5657333.html
Copyright © 2020-2023  润新知