美文网首页
HashMap源码解析

HashMap源码解析

作者: 冠冠从小爱学习 | 来源:发表于2018-02-22 20:55 被阅读0次

前言

HashMap使我们经常使用的数据结构,大家常说map的数据结构就是数组+链表,其中数组指的就是HashMap 中的table,是一个Node类型的数组如下:

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

所说的链表其实就是多个Node相连,具体可以看其实现,Node中含有next成员变量,标记了它的nextNode,如此一个个Node 相连形成链表,如下给出关键内容:

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }

put 方法

先给出put 代码实现:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //2
    }

通过上面代码我们可以看到put的实际实现是调用了putVal,对key 调用hash对其hash,hash实现2处之所以要对h既key.hashcode右移16位是为了让高位bit参与hash运算。

   /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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;  //1 table为null,数组初始化,大小为16
        if ((p = tab[i = (n - 1) & hash]) == null) //2 数组当前位置为null,即没有数据
            tab[i] = newNode(hash, key, value, null);//3 调用node 构造方法生成新的node
        else { //hash到的位置已有node
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) //当前结点的链表头元素就是我们要找的元素,获得当前元素
                e = p;
            else if (p instanceof TreeNode) //1.8后当链表长度大于8后变为treenode 的处理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) { //当前链表长度还小于8,遍历链表
                    if ((e = p.next) == null) {    //如果遍历完未找到已存在的key相同的node,则新增一个node,放在链表尾部,不再使用头插法
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 如果binconut大于7 ,则将链表转化为红黑树,-1是因为从0开始计数
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) //找到对应的元素,跳出循环
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key 如果存在用新值替换掉旧值并返回旧值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); //hashMap是空实现
                return oldValue;
            }
        }
        ++modCount; //增加modCount,遍历时抛出的modCount 不可改变异常与之相关
        if (++size > threshold) //如果当前size 大于下次扩容节点 则扩容,threshold在下面resize 分析中可见
            resize();
        afterNodeInsertion(evict);//hashMap是空实现
        return null;
    }

上面是putVal的实现,其中1处会先判断当前table 是否为null或为空,如果为空就调用resize方法初始化map,resize 实现如下,只需关注关键注释节点:

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //第一次扩容时为null
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) { //如果原有数组长度大于2的30次方,则直接把下一次扩容结点设为2的31次方即最大整数值,返回原有数组不再扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) //如果原长度乘以2后仍小于2的30次方且原有长度大于默认长度16(非初始化),则新长度为原来2倍
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults map初始化,设置初始化容量16,初始化下次扩容结点即16*0.75
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //按照新的扩容长度生成新的node数组
        table = newTab; //将新生成的空数组放回table中去  多线程问题 发生点1
        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) //原有数组j位置 只有一个node 非链表 则直接对该node hash 放入新位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) //1.8后变化,treenode 转化方法
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null; //新数组低位node
                        Node<K,V> hiHead = null, hiTail = null; //新数组高位node,高低位以原有数组长度划分
                        Node<K,V> next;
                        do {  //多线程问题2
                            next = e.next;
                            //如果原来数组hash与原有长度相与为0 ,则证明在新数组中位置不用变,直接将其放入低位node,
                            //只有与2的n次方-1与运算才等于取模,1.8中非常巧妙的实现,后面分析
                            if ((e.hash & oldCap) == 0) { 
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//不为0.则证明需要放入高位head
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //低位链表直接放入新数组j位置,高位链表则放入j+原数组长度的新位置
                        if (loTail != null) { 
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

多线程下可能发生的问题

resize是我们常说的hashmap 线程不安全问题发生的主要地方,很多文章都分析到当多个线程同时执行数组扩容旧数组数组放到新数组操作时有可能会发生头尾指针向同一node,造成resize 源码标2处发生死循环导致cpu飙高,所以需要避免多个线程同时进行put操作,此处不再具体分析;
今天阅读源码发现另一个多线程问题代码中标1处,在旧数组迁移到新数组之前就已经把新数组赋给了table,导致当一个线程扩容操作执行到此处时,其他线程读取map 获得数据为null,具体实验如下:

public static void main(String[] args) {
        HashMap<Integer, Integer> map = Maps.newHashMap();
        map.put(1,1);
        System.out.println("获取到的1的值为"+map.get(1));
        new Thread(() -> {
            System.out.println("find线程启动");
            while (true){
                if (map.get(1)==null){
                    System.out.println("获取到的1为空,发现多线程问题");
                }
            }

        }).start();
        new Thread(() -> {
            for (int i=2;i<600000;i++){
                map.put(i,1);
            }
            System.out.println("数放完了");
        }).start();
    }
结果如下:
获取到的1的值为1
find线程启动
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
获取到的1为空,发现多线程问题
数放完了

通过上述实验,我们可以得出一个线程put,多个线程读取这种操作也是不安全的,多线程还是尽量用concurrentHashMap。

巧妙的&

计算hash值和index时的与运算

index计算示意图,选自网络
  • 调用hashCode计算出一个二进制数值记为h
  • 将h无符号右移16位
  • 将h与右移后的h做异或运算,保证高低位都参与hash计算,得到hash值
  • 将hash值和n-1 做与运算得到index,等价于用hash对n 取模

与运算和 取模操作等价原因,n保证了是2的k次方,所以n-1 的二进制永远是为1,高位为0;所以与hash值进行与运算,参与运算的只有低位为1的值,即相与就会的到hash对n的余数。

扩容时与n相与

与1.7不同,1.8扩容时不再重新通过hash计算每个node的index,而是通过计算node hash和原数组长度的与操作结果是否为0,来判断node在新数组中的位置是否需要在原位置加上原数组长度。


选自网络

a代表扩容前,b代表扩容后,由图可见扩容后n-1是在高位多了一个1,于是决定新的hash值是否与原来相同要看hash对应的位置是0还是1,如果是0则与运算结果和原来相同,否则高位要加1,即原来数组的长度n;
我们需要做的就是要判断对应位置是0还是1,于是源码中的操作是和n 做与运算,因为n永为2的k次方,所以二进制中只有1个bit位是1,所以当与n做与运算就可以判断出hash值的高位是否为0,若结果为0 则代表扩容后位置不用变,若为1 则代表扩容后二进制在高位加了1,即原数组长度n,新的index需要在原index上加n。

&的优势

  • 计算index时改取模操作为与运算,使计算机由耗能巨大的除法运算变为了简单的移位运算;
  • 扩容时& 与操作,避免重复进行通过hash值进行index计算提高了性能,但这一条看起来很奇怪因为计算index 操作也是& 运算,并且得到结果可以直接获取对应index,扩容时和n进行& 与运算还要根据结果判断是否进行加法运算,是否真的提高了性能值得商榷~

get方法

get方法的实现相对简单,show you the code~

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) { //根据hash计算出index 位置
            if (first.hash == hash && // always check first node 
                ((k = first.key) == key || (key != null && key.equals(k)))) //检查index位置的第一个元素是否是目标元素
                return first;
            if ((e = first.next) != null) {//检查后续元素
                if (first instanceof TreeNode)//1.8红黑树相关实现
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {//遍历链表匹配key值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

Map中的entrySet,keySet,values

在没看源码以前,认为这两个方法的实现就是把map中的table转换一下返回一个set;看了源码发现这两个方法实现比较有意思。
以entrySet为例,它返回的是继承了AbstractSet,然后重写了size,clear,iterator,contains等方法的EntrySet。

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();//1.继承HashIterator,重写next方法,调用HashIterator的nextNode
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

这里着重看下iterator的实现,可见代码中返回了new EntrySet(),实现如下:

/**
*调用了HashIterator中的nextNode方法
*/
final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
final Node<K,V> nextNode() {
    Node<K,V>[] t;
    Node<K,V> e = next; //next 是HashIterator的成员变量
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    if ((next = (current = e).next) == null && (t = table) != null) {//遍历每个index下的链表,把下一结点赋值给next变量方便下次调用next
        do {} while (index < t.length && (next = t[index++]) == null);//取table数组中下一个index继续遍历其中链表
    }
    return e;//返回结点
}

keySet和values的Iterator实现和entrySet基本相同,只是它返回分别是KeyIterator()和ValueIterator(),实现如下

final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }
final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

可以看到其实现同样调用了nextNode方法,只不过一个取了key,一个取了value。

通过上面代码可以看出,遍历map的时候最好使用entrySet,可以一次性拿到key,value;如果使用了keySet本质代价和entrySet一样,但还要多出一步get的过程,性能降低~

总结

  • 扩容非常消耗性能,使用hashmap时最好能够提前预估大小,尽量避免频繁扩容
  • 就算是单线程写,多线程读,hashmap一样存在多线程问题,多线程条件下建议使用ConcurrentHashMap
  • 1.8后引入的红黑树优化,在hash冲突严重的情况下性能提升非常明显
  • 遍历时尽可能使用entrySet或直接使用1.8的新方法forEach
  • 这篇文章未涉及红黑树分析,留待后续文章

参考文章:
https://tech.meituan.com/java-hashmap.html

相关文章

网友评论

      本文标题:HashMap源码解析

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