从源码了解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时会出现死循环。

原文地址:https://www.cnblogs.com/hxlr/p/11433914.html