HashMap第1讲——JDK 1.8版本前后的变化

Redis的学习暂时告一段落了,应该还算比较完整,后续再想到其他知识点再补充吧。今天开始就进入Java集合的学习和总结了,我是这样打算的:集合三巨头——HashMap、ConcurrentHashMap和ArrayList会详细介绍(源码+细节),其它比如HashTable、Vector、HashSet、TreeSet等,会根据面试点介绍,必要时也会介绍部分源码。那么下面我们开始介绍集合第一篇文章吧。

ps:HashMap的知识点还是挺多的,所以要分多次介绍,本节就先着重介绍HashMap JDK1.8版本前后的变化。

一、HashMap的作者

开始学习之前,我们先介绍下HashMap的4位作者:

  • Doug Lea:Java并发大神、设计并实现了java.util.concurrent并发包,也参与了部分集合的源码编写,比如HashMap、TreeMap。

  • Josh Bloch:Java集合创办人,《Effective Java》一书作者,前Google首席Java架构师。

  • Arthur van Hoff:JVM的设计者之一,还参与了Java核心类库的设计与开发,比如集合类(HashMap、HashTable等)、I/O类(InputStream、FileInputStream等)等。

  • Neal Gafter:增强for和泛型特性的主要设计者之一,还参与了包括对集合框架的改进和其它API的设计。

毫无疑问,个顶个的大神,不然怎么能设计出如此精妙的HashMap。

二、HashMap的数据结构

ok,进入正题。

首先说下HashMap的特点:

  • 继承抽象类AbstractMap,并实现了Map接口。

  • 非线程安全。

  • key和value都允许为null。

  • 默认初始容量16,扩容的话会扩为原来的2倍。

在Java中,数组和链表是保存数据比较简单的数据结构,数组的特点是寻址容易,插入和删除困难,链表是寻址困难,插入和删除容易。

HashMap在JDK1.8之前的存储结构是数组和链表,如下图:

 在JDK1.8中为了解决hash冲突导致某个链表长度过长,影响put和get的效率,引入了红黑树,如下图:

三、节点变化

在JDK 1.7版本中,每个桶中的元素都需要一个单独的Entry对象:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    //...剩余代码省略
}

在JDK 1.8版本中,用Node代替了旧版本的Entry,hash字段也变成了final,并且为了支持红黑树还引入了TreeNode:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    //...剩余代码省略
}

TreeNode:

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;
    //...剩余代码省略
}

四、扩容时的头插法和尾插法

想必大家也知道,JDK 1.7版本之前,HashMap扩容的时候,会将元素插入链表头部,即头插法,那么在并发扩容的时候,会产生循环引用的问题。不过在JDK 1.8的时候改用了尾插法来解决了这个问题,那么JDK 1.7版本之前采用尾插法的原因是什么?尾插法是如何解决的呢?

4.1 JDK 1.7尾插法

4.1.1 采用头插法的原因

如下图(不够严谨,仅简单表明意思),原来是A->B->C,扩容后就会变为C->B->A(也就是反转链表了)

采用头插法的原因:

因为JDK的开发者认为,后插入的数据使用的概率更高,所以就通过头插法把它们放到队列头部,这样就可以提高查询效率。

源码如下:

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K, V> e = src[j];
        if (e != nul1) {
            src[j] = null;
            do {
                Entry<K, V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                //节点直接作为新链表的根节点
                e.next = newTable[il;
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

4.1.2 产生并发的原因

假设现在有两个线程进行扩容,线程1执行到了Entry<K, V> next = e.next;的时候被卡住了,如下图:

此时线程2开始执行,当线程2完成扩容后:

 

 此时线程1恢复,开始执行:e.next = newTable[il;newTable[i] = e;e = next;,会变为如下图:

接着,进入下一次循环,继续执行:e.next = newTable[il;newTable[i] = e;e = next;,如下图:

 因为此时e!=null且e.next==null,执行最后一次循环,如下图:

可以看到A和B已经循环引用了,当下次get数据的时候,如果get不到,则会一直在A、B之间循环遍历,导致CPU飙升。

4.2 JDK 1.8的尾插法

前面产生循环引用的问题,在JDK 1.8时采用尾插法修复了。本身头插法的本质就是反转原链表,才有出现循环引用的问题,改为尾插法,原来是什么顺序,扩容后就还是什么顺序,就不会出现循环引用的问题。

ps:关于JDK 1.8扩容源码的分析会放到后续的文章中。

五、hash方法

先看源码。

JDK 1.7的hash方法:

final int hash(object k) {
    int h = 0;
    if (useAltHashing) {
        if (k instanceof string) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h = hashSeed;
    }
​
    h ^= k.hashCode();
    
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions(approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK 1.8:

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

可以看到在JDK 1.7的时候,采用了很多位运算和异或运算,主要是为了增加结果的散列性,从而减少碰撞的概率。

JDK 1.8的时候,在保持速度的前提下,提高了hashCode的质量,以减少哈希冲突和提高数据分布的均匀性。

ps:1.8版本的hash方法会在put源码分析时详细介绍。

六、扩容机制

JDK 1.8前后的扩容机制也有很大变化:

  • 在JDK 1.7的时候,扩容是通过resize和transfer两个方法相互配合完成的

  • 在JDK 1.8的时候,把transfer的逻辑放到了resize中,所以1.8版本的resize方法变得很长,主要是因为引入了红黑树,需要考虑树化的过程。

还有就是元素扩容后的位置有变化:

  • JDK 1.7:扩容时,重新计算位置。

  • JDK 1.8:扩容是,需要看原来的hash值新增的bit位是1还是0,0的话位置不变,1的话重新计算(原数组下标位置加上原数组容量)

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橡 皮 人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值