zoukankan      html  css  js  c++  java
  • 从源码了解HashMap和ConcurrentHashMap的基本实现(上)

    面试题1:谈谈HashMap的基本实现?

    HashMap的基本数据结构为自定义的Node内部静态类组成的数组,该静态类存有四个变量,即hash值,key-value对和向后的节点,jdk1.7及以前为单向链表数组,jdk1.8及以后为单向链表数组和红黑树的混合数组,当hash冲突小于8时为单向链表,大于等于8时会转化成黑红树,默认初始最大容量为0,装载因子为0.75f,第一次扩容的默认最大容量为16,以后每次扩容容量翻倍。

    面试题2:为什么HashMap是否线程安全,为什么?

    HashMap线程不安全主要有两种情况,第一种情况:多个线程同时put同一个Hash冲突时,可能会出现后面的线程覆盖前面的线程的值。第二种情况:一个线程扩容,其他线程get同一个Hash冲突时,有可能在get线程种形成闭环,就会进入死循环。

    面试提3:什么是红黑树,有哪些基本特征?

    红黑树是一种自平衡的二叉查找树,主要有以下特征

    1.节点是红色或黑色。

    2.根节点是黑色。

    3.每个叶子节点都是黑色的空节点(NIL节点)。

    4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

    5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    红黑树能保证从根节点到叶子节点的最长路径不会超过最短路径的2倍,写操作时,红黑树通过变色和自旋调整保证红黑树的基本特性。

    源码解析:此源码基于jdk1.8,与jdk1.7有较大的区别,有兴趣的读者可以自己去看基于jdk1.7及以前的源码。

    1.HashMap

    (1)类定义

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable

    HashMap实现了Cloneable(复制), Serializable(序列化)接口

    其主要继承和实现关系如下:

    HashMap-->AbstractMap-->Map

    HashMap-->Map

    前面那条线是对Map的一些优化

    后面那条线是Map的基本线

    (2)主要变量

    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    transient int size;
    
    int threshold;
    
    final float loadFactor;
    
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    
    }

    HashMap的主要存储结构是自定义的内部静态类的键值对数组。

    (3)主要构造方法

    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);
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    由上面的构造方法可知,未设置初始值时,初始容量threshold会默认为0,并设置初始加载因子loadFactor=0.75,该加载因子和初始容量均可设置,loadFactor必须是float类型,且最大容量为1 << 30

    (4)主要方法

    Hash算法

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

     

    HashMap的hash算法为自身hashcode与自身hashcode右移16位做异或运算,如果没有重写hashcode或者是null对象,hash算法的结果是0。

    Put操作

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

     

    put方法比较长,由于篇幅有限,只做原理分析

    1.执行hash算法,取到key的hash值。

    2.检测table是否位空,如果为空会执行resize方法,设置基本的初始值,所以,如果new HashMap不带参时,会让第一次put变复杂。

    3.找到table中对应hash值的节点,其算法是hash值与table长度-1做位与运算,如果没有,会新建一个node节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,next节点为null

    4.如果存在且节点的hash值为新put的hash值且key与该node的key相等,或者equals为true,则记录该节点,value在后面统一设置。

    5.如果存在且属于树状节点,直接按树的规则遍历,如果节点存在则记录该节点,value在后面统一设置,不存在则新建节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null

    6.如果存在且不属于树状节点,且小于8个,则按链表方式遍历直到尾部节点存在则记录该节点,value在后面统一设置,不存在则新建节点,并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null。

    7.如果存在且不属于树状节点,且大于等于8个,则按把链表转换成树,直接按树的规则遍历,如果节点存在则记录该节点,value在后面统一设置,不存在则新建节点并设置该节点的key、valu、hash值为put的key、value和key对应的hash值,记录值为null。

    8.记录节点存在,替换value值并返回被替换的值。

    9.如果size超过threshold * loadFactor,则执行resize方法扩容,返回null。

    扩容

    final Node<K,V>[] resize() {}

     

    resize方法比较长,由于篇幅有限,只做原理分析

    1.设置扩容后的值:如果是第一次扩容且未设置容量大小,设置最大容量为默认最大容量(16),如果并非第一次扩容,则容量为table.length右移一位,容量threshold都为最大容量(默认16)* 装载因子(默认0.75),threshold最大值为Integer.MAX_VALUE

    2.新建Node数组,并将旧数组的值循环遍历重新执行Hash到新数组中的指定位置。

    get操作

    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) {
            if (first.hash == hash && // always check first node
                ((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;
    }

    1.执行hash算法,取到key的hash值。

    2.找到table中对应hash值的节点,其算法是hash值与table长度-1做位与运算,找到其第一个节点,如果该节点不存在,则返回null。

    3.如果该节点是树节点,按树的方式遍历,如果该节点是链式节点,则按链的方式遍历。

    4.遍历找到结果则返回该节点的值,如果遍历结束未找到该值,则返回null。

    remove操作

    public V remove(Object key) {  
    
      Node<K,V> e;  
    
      return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    
    }

    get该节,基本与get操作一致,然后改变节点的指针引用,注意,如果是树状节点,remove后如果头节点的左子树或者右子树为空时,会反向将树转成单链表。

    (5)线程安全性

    HashMa并不是线程安全的主要有两个方面

    1.如果两个线程同时put,此时如果put的值在同一个hash冲突中,同时获取到了最后一个节点时,先执行的线程可能被后执行的线程覆盖。

    2.如果两个线程,一个线程在执行resize操作,另外一个线程执行put操作,如果两个操作在相同的hash冲突中,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环。

  • 相关阅读:
    非空约束
    leetcode208
    leetcode207
    leetcode395
    leetcode116
    leetcode105
    leetcode131
    leetcode73
    leetcode200
    leetcode17
  • 原文地址:https://www.cnblogs.com/hxlr/p/11433914.html
Copyright © 2011-2022 走看看