zoukankan      html  css  js  c++  java
  • 【源码分析】HashMap源码再读-基于Java8

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了。当然,还是先从炙手可热的HashMap,每次读都会有一些收获。当然,JDK8对HashMap有一次优化

    一、一些参数

    我们首先看到的,应该是它的一些基本参数,这对于我们了解HashMap有一定的作用。他们分别是:

    参数 说明
    capacity 容量,默认为16,最大为2^30
    loadFactor 加载因子,默认0.75
    threshold resize的阈值,capacity * loadFactor,元素数量达到这个值后就必须扩容
    treeify_threshold 红黑树的阈值,数组中的某个节点下挂的节点数大于这个值之后,节点的数据结构就会从链表变为红黑树

    二、重要方法

    我们知道,HashMap底层是通过数组+链表来实现的。具体的图网上有很多,我们主要看看几个重要的方法。

    2.1 构造方法

    他的构造方法,最本质的构造方法是:

    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;
        this.threshold = tableSizeFor(initialCapacity);
    }
    

    前面几行就是做一些基本的检查,我们看到,HashMap的最大容量是MAXIMUM_CAPACITY,也就是2^30,我们日常使用时可能达不到这样的容量,但是如果真的需要在Map存储这么多的数据,还是建议存在其他的地方吧。当然,最后一行中时为了计算下一次resize的容量阈值,也就是计算出下一次resize的threshold。

    2.2 计算容量

    这个方法较简单,直接返回的是一个size的值,这个参数的含义就是当前Map中存储的KV对的数量,而不是整个Map的容量。

    public int size() {
        return size;
    }
    

    2.3 put(K,V)

    这个方法是HashMap中比较重要的一个方法,我们仔细分析一下。

    2.3.1 主入口

    首先,方法的入口是这个,也就是我们经常使用的就是这个方法:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    

    我们主要看的,就是put的整个过程。

    2.3.2 hash(key)

    首先就是对我们传入的key,进行hash计算。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    从这个方法,我们可以看到,HashMap的key是可以为null的,如果是null的话,那么我们的hash方法返回的hash值是0。

    如果key不是null,那么调用的是HashMap本身的hashCode方法,也就是我们的bean中自定义的hashCode()。不要以为这样就结束了。我们一般来说,一个key的hash值的范围也就是int的范围(从-2147483648到2147483648),但是HashMap的容量是有限的,必须把hash值能够分散到HashMap的数组中去。HashMap为了key在数组中更加分散,还会进行一次计算,也就是我们看到的第二行的方法。具体这块是如何分配均匀的,可以参考这篇文章

    2.3.3 putVal()

    这个方法就是将KV放到对应的桶中。这个方法的过程,比较清晰。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果之前的数组为空,那么新建一个默认的数组
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果hash值计算出来在数组中的位置上,没有元素,那么直接插入到数组中
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //如果算出的hash存在,而且kv完全一致的话,那么目前什么也不做
                e = p;
            else if (p instanceof TreeNode)
                //如果数组中的元素是红黑树,那么将kv插入到红黑树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //如果hash的桶中,已经存在了一个链表,那么新增一个节点,放到链表的尾部
                        p.next = newNode(hash, key, value, null);
                        //如果链表的长度大于树化的阈值,那么将链表转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果当前数组中存在相同的kv,那么根据是否替换来判断,如果不替换,那么就不替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            //如果空间需要进行扩容,那么进行resize操作
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

    至此,putVal的方法过程基本上清楚了,但是里面有个非常重要的方法,就是resize,我们下面就进入resize方法,看看到底是如何扩容的。

    2.4 resize()

    这个过程是HashMap扩容的过程,也是需要重点理解的一块。
    我们首先看下第一部分。

    2.4.1 确定容量和扩容阈值

    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //如果目前容量已经是最大容量,那么扩容阈值为int的最大值,所有的节点都不需要移动
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果oldCap*2小于最大容量,并且oldCap>=16,扩容为2倍,扩容阈值也*2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {// 默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {//计算新的扩容阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    

    这个过程,主要是确定新的容量和扩容阈值。

    2.4.2 节点移动

    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {//遍历老的数组
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)//如果老的链表,只有一个KV,直接将这个KV放到新的数组链表中
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)//如果老的是红黑树,需要将红黑树中的每个元素都拆分到新的数组和链表/红黑树中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 如果节点是链表,而且不止一个KV的情况下,需要对链表进行处理,处理的过程,光看代码理解起来较困难,需要通过例子来理解
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    

    我们需要知道,坐标点的计算方法是e.hash & (cap-1)

    没有进行扩容时,假设原来的cap=16,也就是默认值,扩容后容量为32。对于hash值为5(二进制为0000 0101)和21(二进制为0001 0101)的元素来讲,计算坐标,也就是和15(二进制为0000 1111)计算后的坐标点都是5,会落到同一个链表中。

    但是扩容后,需要求与的变成了31(二进制为0001 1111),算出来的坐标点分别为5和21,第二个坐标点增加了oldCap的长度。

    此时再看e.hash & oldCap的计算结果,也就是将5和21和16(二进制为0001 0000)求与,得到的结果分别是0和16(!=0)。可以看到,当e.hash & oldCap得到0时,坐标不需要进行变动,也就是不需要在数组中的位置不需要移动。如果结果不为0,需要在原来坐标的位置,增加oldCap。

    这里的lo和hi也就是两个链表,表示的是低位和高位的两条链表。

    三、线程不安全的问题

    我们都知道,HashMap不是线程安全的。这块主要体现在两块:

    3.1 get和put

    这两块都没有加锁,所以可能会导致多线程执行时,出现数据被覆盖的问题。

    3.2 死循环的问题

    这个问题主要出现在resize的过程中,多线程都探测到需要resize时,将链表元素rehash过程中,可能会导致死循环。这个问题参考这篇文章

    至此,源码分析基本结束,我们还可以思考,为什么cap必须是2的幂次,我们应该如何正确的初始化HashMap等。

    欢迎大家关注我的公众号,有各种一线分享。

    qrcode_for_gh_2e415bdf9b4e_258.jpg

  • 相关阅读:
    PHP基础学习笔记(一)
    安装wampserver之后,浏览器中输入localhost页面显示IIS7解决办法
    HTML5常识总结(一)
    AngularJs中的服务
    AngularJs中的directives(指令part1)
    Happy Number——LeetCode
    Binary Tree Zigzag Level Order Traversal——LeetCode
    Construct Binary Tree from Preorder and Inorder Traversal——LeetCode
    Construct Binary Tree from Inorder and Postorder Traversal——LeetCode
    Convert Sorted Array to Binary Search Tree——LeetCode
  • 原文地址:https://www.cnblogs.com/f-zhao/p/10337803.html
Copyright © 2011-2022 走看看