zoukankan      html  css  js  c++  java
  • HashMap源码解析-Java8

    目录

    1.HashMap存储结构图

    2.存储的value是Node类型

    3.hash计算以及确定下标

    4.重要的常量

    5.put操作

    6.get操作

    7.remove操作

    8.链表转红黑树

    9.resize扩容

    10.resize时红黑树拆分

    11.快速失败

    12.HashMap为什么是非线程安全的

     

    1.HashMap的储存结构图

      

      HashMap底层使用数组,每个数组元素存的是Node类型(或者TreeNode),table的每一个位置,又可以称为Hash桶,也就是说,会将相同hash值的元素存放到一个Hash桶中(这里的hash值,是指对key计算的hash值),也就是在Table的下标中相同,为了解决同一个位置有多个元素(冲突),HashMap用来拉链法和红黑树两种数据结构来解决冲突。

    2.存储的value是Node类型

      在HashMap中,存的value不是put的K-V,而是一个Node类型,还有一个TreeNode类型,可以和Node类型相关转换

    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K, V> next;
    
        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }    

    3.hash计算以及确定下标

      元素应该放到table的哪个位置,是通过计算key的hash值,然后与map容量进行“与”操作得到,如下:

    /**
     * 计算key的hash值:
     * 1.如果key为null,则hash值为0;
     * 2.如果可以不为null,否则就是将key的hashCode的和高16位进行异或计算(异或:相同为0,不同为1)
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

      确定数组的下标:

    // 下面是伪代码
    index = (capacity-1) & hash(key);
    table[index] = newNode;

    4.重要的常量

      HashMap中定义了多个常量,非常重要!

    DEFAULT_INITIAL_CAPACITY = 1 << 4
    // HashMap默认的初始容量(16)
    
    MAXIMUM_CAPACITY = 1 <<30
    // HashMap能存的最大元素数量
    
    DEFAULT_LOAD_FACTOR = 0.75
    // 负载因子,默认为0.75,当map中的元素个数达到容量的75%时会触发扩容
    
    TREEIFY_THRESHOLD = 8
    // 当hash碰撞之后写入链表(拉链法),当链表的长度达到该阈值时,则可能会转化为红黑树
    // 注意链表转换为红黑树,还与MIN_TREEIFY_CAPACITY有关
    
    UNTREEIFY_THRESHOLD = 6
    // 当红黑树的元素个数小于该值时,转换为链表形式
    
    MIN_TREEIFY_CAPACITY = 64
    // 当链表的长度超过了阈值(TREEIFY_THRESHOLD),且map的容量不小于64,链表将会转换为红黑树
    // 这里的64,是指的map的容量(hash桶的数量),也就是说,当容量少于64时,即使超过树化阈值,也不会树化
    

    5.put操作

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
    
        // 初始状态,HashMap为空,则需要扩容,n为扩容后的容量
        if ((tab = table) == null || (n = tab.length) == 0) {
            n = (tab = resize()).length;
        }
    
        if ((p = tab[i = (n - 1) & hash]) == null) {
            // 要放入的位置没有其他项(没有冲突),则直接放入该位置
            tab[i] = newNode(hash, key, value, null);
        } else {
            // 计算后,要放入的位置已经有了其他项,需要解决冲突(拉链法或者红黑树)
            Node<K, V> e;
            K k;
    
            // 上一步操作后,p指向的该"桶"的第一个Node,判断位置是否匹配,如果位置匹配,且key相同,表示是put的数据已经存在,直接覆盖即可
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                e = p;
            } else if (p instanceof TreeNode) {
                // 如果p指向的是TreeNode,也就是红黑树存储的节点,那么就将新增元素加入到红黑树中
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            } else {
                // p指向的是链表头结点,则利用尾插法,将新节点插入到末尾(遍历过程中发现相同节点则进行覆盖)
                for (int binCount = 0; ; ++binCount) {
                    // 走到尾节点
                    if ((e = p.next) == null) {
                        // 将新数据查到最后
                        p.next = newNode(hash, key, value, null);
                        // 判断链表的长度是否达到树化的阈值,如果是,则将链表转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
                            // 注意并不一定会转换为红黑树,还与tab的长度有关,tab.length<MIN_TREEIFY_CAPACITY时,仍旧采取扩容,而非树化
                            treeifyBin(tab, hash);
                        }
                        break;
                    }
    
                    // 如果是已经存在的节点,则中断循环,后面将进行覆盖value
                    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);
                return oldValue;
            }
        }
        
        // 计数加一(用来快速失败)
        ++modCount;
        if (++size > threshold) {
            resize();
        }
        afterNodeInsertion(evict);
        return null;
    }
    

    6.get操作

    public V get(Object key) {
        Node<K, V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    /**
     * get的时候,最关键的就是,先根据key的hash值找到桶位置,然后在根据key来查找
     */
    final Node<K, V> getNode(int hash, Object key) {
        Node<K, V>[] tab;
        Node<K, V> first, e;
        int n;
        K k;
    
        // 根据key进行hash后的位置存在数据,如果不存在,则直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            // 根据hash和key进行判断第一个节点是否为要找的元素,如果是,则返回第一个节点
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {
                return first;
            }
    
            if ((e = first.next) != null) {
                // 如果节点时红黑树的节点类型,则遍历红黑树,进行查找
                if (first instanceof TreeNode) {
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                }
    
                // 遍历链表进行查找
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        return e;
                    }
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

    7.remove操作

      remove有两个接口,remove(key)、remove(key,value),内部都是调用一个removeNode方法,如下:

    public V remove(Object key) {
        Node<K, V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }
    
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
    
    final Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, index;
        // map不为空,且hash对应的位置不为空,才进行查找,否则认为未找到,返回null
        if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
            Node<K, V> node = null, e;
            K k;
            V v;
            // 匹配hash地址的第一个节点是否匹配,hash和key都匹配,则证明找到了要删除的元素
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
                node = p;
            } else if ((e = p.next) != null) { // 第一个节点不匹配,则进行遍历查找
                // 遍历红黑树
                if (p instanceof TreeNode) {
                    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
                } else {
                    // 遍历链表
                    do {
                        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
    
            // 如果node为null,证明未找到key对应的元素
            // node不为null,则根据调用的remove(key)还是remove(key,value)来判断
            if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
                // 要删除的节点匹配,如果是树节点类型,则从树中删除节点
                if (node instanceof TreeNode) {
                    ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                } else if (node == p) {
                    // 要删除的节点时第一个节点时,直接将头结点的下一个节点往前提一个位置(旧头节点被删除)
                    tab[index] = node.next;
                } else {
                    // 非头结点,修改指针,将下一个节点赋给父节点的next
                    p.next = node.next;
                }
    
                // 修改次数加一,元素数量减一
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }  

    8.链表转红黑树

      上面在put的时候,如果链表的长度超过树化阈值,则会触发树化操作,具体代码如下:

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
    
        // 如果map的容量(数组的长度)为0,或者小于MIN_TREEIFY_CAPACITY(默认64),则进行扩容操作,而不进行转换红黑树
        // 底层数组,也称为hash桶,也就是说hash桶的数量小于64时,则会进行扩容操作
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
            resize();
        } else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                // 将链表节点转换为红黑树节点
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
    
            // 转换红黑树的操作
            if ((tab[index] = hd) != null) {
                hd.treeify(tab);
            }
        }
    }
    

      

    9.resize扩容

      扩容操作比较复杂:

    final Node<K, V>[] resize() {
        Node<K, V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    
        // threshold表示触发扩容的阈值(size >= capacity * load factor时会扩容)
        int oldThr = threshold;
        int newCap, newThr = 0;
    
        // oldCap大于0证明已经对map进行过操作,并非刚创建map的时候
        if (oldCap > 0) {
            // 如果当前容量允许的大于最大容量,则将阈值设置为整数最大值,不会进行复制操作
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
                // 如果2倍旧容量未超过允许的最大容量,并且旧容量达到了默认的初始容量16,则新的扩容阈值设置2倍的旧容量
                newThr = oldThr << 1; // double threshold
            }
        } else if (oldThr > 0) {
            // 使用HashMap(capacity)或者HashMap(capacity, loadFactor)创建map
            // 这是初次扩容,新容量设置为threshold,也就是capacity*loadFactor
            newCap = oldThr;
        } else {
            // 第一次扩容,使用new HashMap()这种方式创建map,容量和负载因子都使用默认
            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;
    
        // 申请一个新的数组
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;
    
        // 下面是将旧数组中的元素复制到新申请的数组中
        // 因为在旧数组中节点的索引计算方式:oldIndex=(oldCapacity - 1) & key.hash,
        // 当数组的容量发生变化后,需要重新确定节点的索引,新的节点位置有两种可能:
        // 1.newIndex=oldIndex,索引不变,前提是key.hash & oldCapacity结果为0
        // 2.newIndex=oldIndex+oldCapacity,不是第一种情况,就是第二种情况
        if (oldTab != null) {
            // 遍历旧数组(oldCap长度)
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                //
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null) {
                        // 确定新的位置,存入
                        newTab[e.hash & (newCap - 1)] = e;
                    } else if (e instanceof TreeNode) {
                        // 将红黑树的节点进行拆分,将树中的每个节点都存入新位置,同时判断是否需要进行树转链表
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    } else { // preserve order
                        // 遍历链表,将链表分为两部分,一部分是索引不变,一部分的新索引是oldIndex+oldCapacity
                        // 然后将链表放入对应的数组中
                        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;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

      

    10.resize时红黑树的split拆分

      和链表一样,红黑树中的元素也需要挨个确定新索引位置,同样是分为2部分,一部分是索引不变,一部分的新索引为oldIndex+oldCapacity。

      注意,split是HashMap中的内部类TreeNode的方法,而不是HashMap的方法。

    /**
     * 扩容时,对同一个hash桶中的元素(红黑树)进行拆分,有可能拆分为两部分
     * part1.节点的hash和原数组的容量与之后为0 -> 移到新表后,索引和旧表保持不变
     * part2.节点的hash和原数组的容量与之后为0 -> 移到新表后,新索引为"oldIndex+oldCapacity"
     * 这两部分,在做完拆分后,判断是否需要将树转换为链表,如果各自的数量未超过UNTREEIFY_THRESHOLD(默认为6),则需转换为链表
     *
     * @param map   hashMap实例本身
     * @param tab   扩容新申请的数组
     * @param index 本次要拆分的下标索引(对应旧数组)
     * @param bit   旧数组的容量
     */
    final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
        TreeNode<K, V> b = this;
        // Relink into lo and hi lists, preserving order
        TreeNode<K, V> loHead = null, loTail = null; // loHead链着索引不变的节点
        TreeNode<K, V> hiHead = null, hiTail = null; // hiHead链着索引改变的节点
        int lc = 0, hc = 0;
        for (TreeNode<K, V> e = b, next; e != null; e = next) {
            next = (TreeNode<K, V>) e.next;
            e.next = null;
    
            // 如果当前节点和原数组的容量与之后为0,则扩容后的索引位置和与在旧表保持一致
            if ((e.hash & bit) == 0) {
                if ((e.prev = loTail) == null)
                    loHead = e;
                else
                    loTail.next = e;
                loTail = e;
                ++lc;
            } else {
                // 如果当前节点和原数组的容量与之后不为0,则扩容后的索引位置为"oldIndex+oldCapacity"
                if ((e.prev = hiTail) == null)
                    hiHead = e;
                else
                    hiTail.next = e;
                hiTail = e;
                ++hc;
            }
        }
    
        if (loHead != null) {
            if (lc <= UNTREEIFY_THRESHOLD)
                tab[index] = loHead.untreeify(map);
            else {
                tab[index] = loHead;
                if (hiHead != null) // (else is already treeified)
                    loHead.treeify(tab);
            }
        }
        if (hiHead != null) {
            if (hc <= UNTREEIFY_THRESHOLD)
                tab[index + bit] = hiHead.untreeify(map);
            else {
                tab[index + bit] = hiHead;
                if (loHead != null)
                    hiHead.treeify(tab);
            }
        }
    }
    

      

    11.快速失败

      因为HashMap是非线程安全的容器,也就是说,多线程访问的时候会有问题,比如一个有一个线程在遍历Map,每遍历一项,执行某个操作(假设耗时2秒),此时另外一个线程对Map做了修改(比如删除了某一项),这个时候就会出现数据不一致的问题,此时HashMap发现这种情况,读线程就会抛出ConcurrentModificationException,防止继续读取脏数据,这个过程叫做快速失败。

      实现快速失败,就是使用HashMap中的modCount变量,该变量存储的是map中数据发生变化的次数,每发生一次变化,则modCount加一,比如put操作后modCount会加1;一个线程如果要遍历HashMap,会在遍历之前先记录modCount值,然后每迭代一次(访问下一个元素)时,先判断modCount值是否和最初的modCount是否相等,如果相等,则证明map未被修改过,如果不相等,则证明map被修改过,那么就会抛出ConcurrentModificationException,实现快速失败。

    12.为什么HashMap是非线程安全的

      当容器发生扩容的时候,map中的所有元素都会进行重新确认索引位置(reindex),如果是链表或者红黑树,还会进行split,这个过程中,如果更改map中的元素,则可能会引起异常。

      如果要使用线程安全的map,可以考虑ConcurrentHashMap。

      标注原文地址:https://www.cnblogs.com/-beyond/p/13088591.html

  • 相关阅读:
    Hibernate save, saveOrUpdate, persist, merge, update 区别
    Eclipse下maven使用嵌入式(Embedded)Neo4j创建Hello World项目
    Neo4j批量插入(Batch Insertion)
    嵌入式(Embedded)Neo4j数据库访问方法
    Neo4j 查询已经创建的索引与约束
    Neo4j 两种索引Legacy Index与Schema Index区别
    spring data jpa hibernate jpa 三者之间的关系
    maven web project打包为war包,目录结构的变化
    创建一个maven web project
    Linux下部署solrCloud
  • 原文地址:https://www.cnblogs.com/-beyond/p/13088591.html
Copyright © 2011-2022 走看看