欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JDK1.8源码分析:HashMap

程序员文章站 2022-06-04 19:25:36
...

数据结构

在JDK1.8之前,HashMap是基于链式哈希实现的,而在JDK1.8之后,为了提高冲突节点的访问性能,在链式哈希实现的基础上,在哈希表大小超过64时,针对冲突节点链条,如果节点数量超过8个,则升级为红黑树,小于等于6个时,则降级为链表结构。

链式哈希

  • 链式哈希是一个数组结构,数组元素为链表或者红黑树。如下为HashMap的内部数据存储结构,也是链式哈希的实现。其中Node为一个key的hash值相同的数据的链表(或红黑树)的头节点,即为冲突节点的链表。

    /**
     * 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;
    

链表节点

  • 如下为链表节点的数据结构设计:包括key,value,key的hash值,当前节点在链表中的下一个节点next。

  • 该链表内的所有节点的key的hash值是相同的,链表头结点存放在哈希表table中,基于hash值与table大小获取该链表头结点在table中的位置下标。

    /**
     * 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;
    
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
    
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
    
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    

红黑树节点

  • 与链表节点一样,红黑树中存放key的hash值相同的节点集合,其中红黑树根节点root存放在哈希表table中,对应的table数组下标也是基于key的hash值和数组table大小获取。

    /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
    
        /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
        
        ...
        
    }
    

核心设计

table数组容量与阈值

/**
 * 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;
  • DEFAULT_INITIAL_CAPACITY: table数组默认大小为16,即在应用代码中,创建一个HashMap时,不指定容量,则之后在第一次添加数据到该HashMap,首先分配一个大小为16的数组table。

  • MAXIMUM_CAPACITY:数组最大大小,超过则不再进行拓容,大小为2的30次方。

  • DEFAULT_LOAD_FACTOR:数组拓容阈值,即在当前数组存放的数据节点达到容量的0.75时,则进行数组拓容,具体拓容逻辑在resize方法定义。

构建红黑树阈值

  • 默认哈希表对应的数组的每个元素是一个链表,具体为存放链表头节点,而在冲突节点较多时,即存在较多key的hash值相同的元素,则会导致链表过长,在应用代码中获取某个key的值value,时间复杂度就会增加,即默认的O(1)变为了O(N),其中N为该链表长度。

  • 所以为了解决这种情况下的性能问题,JDK1.8提供了从链表转为红黑树,或者从红黑树还原为链表的设计,在红黑树中获取某个节点的时间复杂度为O(logN),具体阈值如下:

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;
    
    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    
    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
  • TREEIFY_THRESHOLD:构造红黑树的阈值,默认为8,即链表的元素个数超过8个的时候,则将链表转为红黑树,不过前提是数组大小大于MIN_TREEIFY_CAPACITY。

  • MIN_TREEIFY_CAPACITY:触发红黑树化的前提条件,默认值为64,即哈希表数组table的大小超过64的时候,如果某个数组元素对应的链表的长度超过TREEIFY_THRESHOLD,则将该数组元素(即链表头结点)对应的链表转为红黑树结构。

  • UNTREEIFY_THRESHOLD:从红黑树退化为链表的条件,默认为6,即当红黑树中节点个数不超过6时,则退化为链表。

核心操作设计

数组大小capacity调整

  • HashMap内部存储数据所用的链式哈希表是基于数组实现的,即数组 + 链表或者数组 + 红黑树,所以在往HashMap存放数据之前,需要指定数组大小来创建这个数组。

  • 在存放某个数据时,需要根据该数据的key的hash值与数组大小取模,从而确定该数据所在的链表(具体为链表头结点)在数组中的位置下标。在HashMap的设计中,为了提高性能,采用的是位运算的方式,而不是使用%的方式取模,具体方式如下:

    (capacity - 1) & hash
    
  • 这种方式的实现基础为capacity的值为2的N次方,则capacity - 1的值对应二进制就全是1了,如16的二进制为:10000,15的二进制为:01111,则01111与任何数字的与运算的结果为0到15,如:

    01111 & 00010 = 00010,即15与2取模等于2
    01111 & 100001 = 00001,即15与33取模等于1
    
  • 所以在指定HashMap的容量capacity时,则内部会将capacity调整为2的N次方,即调整后的capacity大于或等于应用代码指定的capacity。调整实现为:在tableSizeFor方法定义,如下:

    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);
    }
    
    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    

put设值

  • put操作往HashMap存放数据,在内部主要是在putVal方法定义实现逻辑。具体过程请看代码注释:

    /**
     * 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) {
                   
        // tab:指向哈希表table
        // p:遍历哈希表table时,存放遍历到的数组元素
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        
        // table为null,还没初始化或者大小为0,
        // 则调用resize先创建数组或者对数组进行拓容
        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;
            
            // 1.如果数组元素(链表头结点)就是该节点,
            // 即此次put操作是更新,则更新值value即可;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 2. 或者之前已经将链表升级为了红黑树,
            // 则往该红黑树插入该节点;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            
             // 3. 或者链表头结点不是该元素,
             // 则遍历链表在链表尾部插入该节点,
             // 在链表尾部插入之后,
             // 需要检查一下是否需要将该链表升级为红黑树
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        // 检查链表长度是否超过了8
                        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;
                }
            }
            
            // 更新节点值value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                
                // 直接返回更新之前的值
                // 更新操作不是结构性修改,故不需要往下继续执行。
                return oldValue;
            }
        }
        
        // 记录结构性修改(增加或删除节点)次数
        // 用于实现迭代器iterator的fail-fast快速失败
        ++modCount;
        
        // 如果插入后,哈希表大小超过了阈值,
        // 则resize拓容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    

resize扩容

  • resize操作主要是对哈希表数组table进行拓容或初始化,拓容为每次增大为原来的2倍,从而保证容量capacity为2的N次方的设计。

  • 拓容阈值threshold:threshold = capacity * load factor,即默认为容量capacity的0.75,当存放的元素达到容量的3/4时,进行拓容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;
        int oldThr = threshold;
        int newCap, newThr = 0;
        // 原来的容量大于0,即哈希表数组table已经存在了的
        if (oldCap > 0) {
        
            // 当前容量已经是最大容量,则直接返回,不再进行拓容。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            
            // 将容量和拓容阈值都拓展为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        // 旧容量小于0,则容量设值为拓展阈值
            newCap = oldThr;
        
        // 初始化哈希数组,使用默认值
        else {               // zero initial threshold signifies using defaults
            
            // 容量等于初始大小16
            newCap = DEFAULT_INITIAL_CAPACITY;
            
            // 拓容阈值等于16*0.75=12
            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"})
        
            // 创建一个新的哈希数组,容量为原来的2倍
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
        
            // 遍历旧的哈希数组,获取元素放到拓容后,新的哈希数组中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // e已经存放了原来的数组元素,
                    // 则将旧数组的元素置为null,便于GC回收
                    
                    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);
                    
                    // 将链表(具体为链表头结点)
                    // 复制到新数组中
                    // 由于数组拓容后,大小capacity变大了,
                    // 故单个链表中的元素的hash与capacity取模会发生变化,
                    // 故旧数组的某个数组元素,
                    // 对应的链表也要拆分成两条链表,放在新数组的两个位置中。
                    
                    // 同时需要保持链表中元素的位置,
                    // 即链表前半部分对应的新的子链表对应的新数组中的位置为
                    // 该链表在旧数组的位置下标
                    // 链表后半部分对应的新的子链表放在
                    // 新数组后面的一个位置下标
                    else { // preserve order
                        
                        // lo为链表的前半部分
                        // hi为链表的后半部分
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            
                            // e.hash与oldCap,而不是oldCap-1,而oldCap为2的N次方,二进制为,
                            // 如16的二进制为10000
                            // 则 e.hash & oldCap == 0,表示e所在链表,
                            // 在拓展后在新数组中的位置,
                            // 等于原来在旧数组中的,
                            // 所以放到lo部分。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            
                            // 拓容后,hash值与新的capacity取模不等于与旧的capacity的,
                            // 故放到hi部分。
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        if (loTail != null) {
                            loTail.next = null;
                            // lo链表放到新数组下标j中
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                        
                            // hi链表放到新数组下标j+oldCap中
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

treeifyBin构造红黑树

  • 主要用来将某个数组元素对应的链表,转为红黑树结构,从而解决链表长度过长时的访问性能问题。

    /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        
        // 当哈希表为null即还没初始化,
        // 或者哈希表对应的数组大小小于64时,
        // 则resize,先不将该链表转为红黑树,
        // 即使该链表长度超过了8,
        // 主要是因为当前的哈希表还是比较小的。
        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);
        }
    }
    

clear情况

  • 清空HashMap,具体为将哈希表数组table的每个元素置为null,则该元素对应的链表或红黑树则没有被引用了,可以被GC回收。

    /**
     * Removes all of the mappings from this map.
     * The map will be empty after this call returns.
     */
    public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }
    

哈希表节点Node集合

EntrySet:哈希表所有节点集合

  • 从数组的每个元素对应的链表中取出链表节点放到该集合

keySet:哈希表key集合

  • 哈希表所有节点的key集合

Values:哈希表value集合

  • 哈希表所有节点的value集合

链表迭代器基类:HashIterator

  • 主要被以上集合用来遍历哈希表数组,然后遍历每个数组对应的链表,从而获取所有的数据,即Node节点,然后放到集合中。

  • 派生类包括:KeyIterator,ValueIterator,EntryIterator,其中下一个节点都依赖nextNode方法,从链表头结点开始,获取该链表的下一个节点。

  • 迭代器是快速失败fail-fast的,即在使用过程中,如果往HashMap中添加或删除了数据,则抛异常退出。

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
    
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            
            // 初始化为null
            current = next = null;
            
            // 从数组table第一个数组元素开始
            index = 0;
            if (t != null && size > 0) { // advance to first entry
            
            // 遍历哈希表数组table,
            // 获取第一个数组节点(即某个链表头节点)
            // 不为null的节点,
            // 即整个哈希表从数组和链表两个角度,
            // 第一个节点Node作为迭代的开始节点。
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }
    
        public final boolean hasNext() {
            return next != null;
        }
    
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            
             // 并发进行了结构性修改
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            
            // 先遍历数组的元素对应的链表
            // 遍历完该链表后,
            // 遍历数组下一个元素对应的链表
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
                
            // 并发进行了结构性修改
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }
    

线程安全

  • HashMap不是线程安全的,故如果需要保证多线程访问的并发安全性,需要使用ConcurrentHashMap,或者使用Collections.synchronizedMap方法来对HashMap进行包装成类型为SynchronizedMap的,线程安全的map。
  • ConcurrentHashMap主要是基于CAS来实现乐观锁来实现线程安全;
  • SynchronizedMap的内部实现,主要是通过synchronized关键字结合一个对象锁mutex,来对会产生并发问题的操作,如get,put,remove,clear,contains操作进行同步操作。