美文网首页
【基础】- HashMap 深入

【基础】- HashMap 深入

作者: lconcise | 来源:发表于2020-12-16 20:22 被阅读0次

    HashMap 的数据结构

    在 Java 中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址 容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。上面我们提到过, 常用的哈希函数的冲突解决办法中有一种方法叫做链地址法,其实就是将数组和链表组合在 一起,发挥了两者的优势,我们可以将其理解为链表的数组。


    image.png

    我们可以从上图看到,左边很明显是个数组,数组的每个成员是一个链表。该数据结构 所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分 配到不同的链表中去,反过来我们也正是通过这些特征找到正确的链表,再从链表中找出正 确的元素。其中,根据元素特征计算元素数组下标的方法就是哈希算法,即本文的主角 hash()函数(当然,还包括 indexOf()函数)。

    哈希类集合的三个基本存储概念

    • table
      存储所有节点数据的数组
    • slot
      哈希槽。即table[i]这个位置
    • bucket 哈希桶。
      table[i]上所有元素形成的表或树的集合。

    扩容 负载因子 0.75
    length table 数组的长度
    size 成功通过put方法添加到HashMap中的所有元素的个数
    hashCode Object.hashCode()返回的int值,尽可能地离散均匀分布
    hash Object.hashCode()与当前集合的table.length进行位运算的结果,以确定哈希槽的位置

    HashMap 高并发,新增对象丢失

    key value 允许为null

    JDK1.7 数组+链表
    JDK1.8 数组+链表+红黑树

    JDK1.7 的死链问题

    /**
     * HashMap(JDK7) 死链.
     * <p>
     * 启动10万个线程,以System.nanoTime()返回的Long值为Key,自定义对象EasyCoding为Value,运行环境是JDK7。
     *
     * @author liusj
     * @date 2020/12/11
     */
    public class HashMashEndlessLoop {
    
        private static HashMap<Long, EasyCoding> map = new HashMap<>();
    
        public static void main(String[] args) {
            for (int i = 0; i < 10000; i++) {
                (new Thread() {
                    @Override
                    public void run() {
                        map.put(System.nanoTime(), new EasyCoding());
                    }
                }).start();
            }
        }
    }
    
    class EasyCoding {
    }
    
    

    问题

    1. HashMap 和 ConcurrentHashMap 的区别?

    ConcurrentHashMap 和 HashMap 的实现方式不一样,虽然都是使用桶数组实现 的,但是还是有区别,ConcurrentHashMap 对桶数组进行了分段,而 HashMap 并没有。 ConcurrentHashMap 在每一个分段上都用锁进行了保护。HashMap 没有锁机制。 所以,前者线程安全的,后者不是线程安全的。

    2. HashMap 的容量、扩容

    主要成员变量:
    * transient int size
    记录了 Map 中 KV 对的个数
    * loadFactor
    装载因子,用来衡量 HashMap 满的程度。loadFactor 的默认值为 0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;)
    * threshold
    临界值,当实际 KV 个数超过 threshold 时,HashMap 会将容量 扩容,threshold=容量*装载因子
    * capacity
    容量,如果不指定,默认容量是 16(static final int DEFAULT_I NITIAL_CAPACITY = 1 << 4;)

    size 和 capacity
    HashMap 中的 size 和 capacity 之间的区别其实解释起来也挺简单的。我们知道, HashMap 就像一个“桶”,那么 capacity 就是这个桶“当前”最多可以装多少元素,而 size 表示这个桶已经装了多少元素。

    loadFactor和threshold
    前面我们提到过,HashMap 有扩容机制,就是当达到扩容条件时会进行扩容,从 16 扩容到 32、64、128... 那么,这个扩容条件指的是什么呢?

    其实,HashMap 的扩容条件就是当 HashMap 中的元素个数(size)超过临界值( threshold)时就会自动扩容。

    在 HashMap 中,threshold = loadFactor * capacity。

    loadFactor 是装载因子,表示 HashMap 满的程度,默认值为 0.75f,设置成 0.75 有一个好处,那就是 0.75 正好是 3/4,而 capacity 又是 2 的幂。所以,两个数的乘积都 是整数。

    对于一个默认的 HashMap 来说,默认情况下,当其 size 大于 12(16*0.75)时就会触 发扩容。

    总结
    HashMap 中 size 表示当前共有多少个 KV 对,capacity 表示当前 HashMap 的容量是多少,默认值是 16,每次扩容都是成倍的。loadFactor 是装载因子,当 Map 中元素个数超过 loadFactor* capacity 的值时,会触发扩容。loadFactor* capacity 可以用 threshold 表示。

    3. 为什么HashMap的默认容量设置成16

    hash实现
    具体实现上,由两个方法int hash(Object k)和int indexFor(int h,int length)来实现。

    • hash: 该方法主要是将Object转换成一个整型
    • indexFor: 该方法主要是将hash生成的整型转换成链表数组中的下标
        static int indexFor(int h, int length) {
            // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
            return h & (length-1);
        }
    

    indexFor 方法其实主要是将 hashcode 换成链表数组中的下标。其中的两个参数 h 表示元素的 hashcode 值,length 表示 HashMap 的容量。那么 return h & (length-1) 是什么意思呢?

    其实,他就是取模。Java 之所有使用位运算(&)来代替取模运算(%),最主要的考虑就 是效率。

    位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行 操作,不需要转成十进制,因此处理速度非常快。

    扩容

    总结
    HashMap 作为一种数据结构,元素在 put 的过程中需要进行 hash 运算,目的是计算出该元素存放在 hashMap 中的具体位置。

    hash 运算的过程其实就是对目标元素的 Key 进行 hashcode,再对 Map 的容量进行 取模,而 JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求 Map 的容量一定得是 2 的幂。

    而作为默认容量,太大和太小都不合适,所以 16 就作为一个比较合适的经验值被采用 了。

    为了保证任何情况下 Map 的容量都是 2 的幂,HashMap 在两个地方都做了限制。

    首先是,如果用户制定了初始容量,那么 HashMap 会计算出比该数大的第一个 2 的 幂作为初始容量。

    另外,在扩容的时候,也是进行成倍的扩容,即 4 变成 8,8 变成 16。

    4. 为什么建议设置 HashMap 的初始容量,设置多少合适

    1. 要设置HashMap的初始化容量
      HashMap 有扩容机制,就是当达到扩容条件时会进行扩容。HashMap 的扩容条件 就是当 HashMap 中的元素个数(size)超过临界值(threshold)时就会自动扩容。在 HashMap 中,threshold = loadFactor * capacity。

    所以,如果我们没有设置初始容量大小,随着元素的不断增加,HashMap 会发生多 次扩容,而 HashMap 中的扩容机制决定了每次扩容都需要重建 hash 表,是非常影响性能的。

    1. HashMap 初始化容量设置多少合适
      这里我们可以参考 JDK8 中 putAll 方法中的实现的,这个实现在 guava(21.0 版本) 也被采用。

    这个值的计算方法就是:

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

    比如我们计划向 HashMap 中放入 7 个元素的时候,我们通过 expectedSize / 0.75 F + 1.0F 计算,7/0.75 + 1 = 10 ,10 经过 JDK 处理之后,会被设置成 16,这就大大的 减少了扩容的几率。

    Map<String, String> map = Maps.newHashMapWithExpectedSize(7);
    

    参考书籍:《Java工程师成神之路》

    相关文章

      网友评论

          本文标题:【基础】- HashMap 深入

          本文链接:https://www.haomeiwen.com/subject/wewwwktx.html