HashMap以及concurrentHashMap剖析


在JDK1.8之后HashMap以及concurrentHashMap的实现与JDK1.7中非常不同,JDK1.8对此进行了很大的优化,尤其是coucurrentHashMap。在1.8中,concurrentHashMap的结构与HashMap已经非常的相似了,我们先来看看HashMap。

HashMap

HashMap的主干是一个Entry数组,一个Entry包含了key,value,next和hashCode,结构如下

在JDK1.7中,每个数组的位置是由链表组成的,当对hash值进行散列计算数组位置时,如果多个Entry计算出了相同的数组位置即发生了冲突,就会用一个链表进行存储,这里需要说明一下这个散列计算,HashMap中用到的散列算法是用hash值与数组的长度-1进行按位与操作,在HashMap的构造函数中我们可以知道,如果我们指定了初始大小,构造函数会找到一个大于等于我们指定的初始大小的2的幂次方的值

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

也就是说,当我们指定初始容量为5时,最后容量会变为8,永远是2的幂次方,那么为什么初始容量需要为一个2的幂次方的值呢?
这是根据散列算法来制定的,用2的幂次方-1去对hash值取模能够减少hash冲突,使得Entry能够尽量的分布在不同的节点上。举个例子,当前有两个hash值分别为8和9的数,分别用当数组长度为16和15去做散列计算,当数组长度为16时,这两个不同的hash值有着不同的散列值,分别为00001000和00001001,也就是说这两个值肯定不会在同一个数组节点上;当数组长度为15时,有着相同的散列值,都为00001000,也就是说在同一个数组节点上,那么就得放在一个链表上。我们知道当链表长度足够长时,是非常消耗性能的,需要一个节点一个节点去访问,复杂度为O(n),所以在JDK1.8时,就出现了红黑树结构来解决这个问题,当链表的长度大于8时,链表会转换为红黑树的结构来解决查询效率低的问题。

红黑树

在了解红黑树之前我们需要了解一下二叉查找树,什么是二叉查找树呢。二叉查找树就是为了提高查找效率而出现的一种树结构,每个二叉查找树的父节点都比它的左边儿子大,比它的右边儿子大小。
在这里插入图片描述

但是这种查找树有一个问题,当进行插入、删除操作时,这个树会变成一颗斜树,无法保证平衡
在这里插入图片描述

所以这时就出现了红黑树,当对一颗红黑树进行插入、删除等操作时,树会根据情况来进行变色、旋转,并且一颗红黑树具有以下几种特征:

  1. 节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点都是黑色的空节点
  4. 每个红色节点的两个子节点都是黑色的
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

那么前面这个包含8,9,12的二叉查找树转换为一个红黑树就是这样的
在这里插入图片描述

那么我们现在来插入一个数字7,来看看红黑树是如何通过变色,旋转来保证平衡的。
在这里插入图片描述

我们看到,插入后红色节点8的下面还有一个红色的节点7,这明显是不符合每个红色节点的两个子节点都是黑色的这条规则的,所以我们需要进行变色操作。
在这里插入图片描述

变色之后,我们发现这有不符合第五条规则,因为右边的叶子节点的null节点到根节点有2个黑色节点,而左边有三个。所以我们需要再进行变色
在这里插入图片描述

这样,这棵树就满足了红黑树的五个规则了,但是插入了一个7并没有用到旋转操作,这时我们就在插入一个6
在这里插入图片描述

这样经过变色、旋转、变色操作后我们得到了红黑树
注意:在实际的实现中程序不一定是按照我这样的顺序,我只是提供了一个思路
我们从中可以看到红黑树为了使树平衡,会经过大量的旋转变色,所以可想而知红黑树的缺点就是实现复杂,插入删除效率不高。所以在HashMap的实现中,只有当一个数组位置中Entry节点数量大于8时,才会将链表转换为红黑树,当节点数量小于6时,又会将红黑树转换为链表。

扩容

在HashMap中,有两个参数十分重要,一个是初始容量,一个是加载因子(填充因子)。初始容量已经不用说了,那么什么是加载因子呢,当HashMap中插入后Entry节点的数量>=HashMap的大小*加载因子(默认值为0.75),那么HashMap就会进行扩容操作(扩容后HashMap大小为原来的两倍)。扩容操作有很多好处,一是可以动态的设置HashMap的大小,不必像数组那样固定大小,二是可以使单个数组位置上链表或者红黑树上的节点变小,因为扩容操作会重新对Entry的hash进行散列计算。但是正是因为对Entry进行重新的散列计算,使得扩容这个操作变得开销非常大,所以在《阿里巴巴开发规范手册》(好像叫这个名字)中,他们建议使用HashMap时尽量指定HashMap的初始容量,以便减少HashMap的扩容操作。

HashMap在多线程下的问题

总所周知,HashMap是不能在多线程下使用的,因为HashMap不是线程安全的,那么为什么HashMap不是线程安全的呢?还是因为这个扩容操作出了问题,在JDK1.7中,HashMap的扩容操作在创建了一个容量为原始容量的两倍的新表之后,在移动旧表到新表的过程中,是使用头插的方法,也就是插入新表的对应数组位置是从头往后插的,这样一来,如果有两个线程同时扩容操作时,就有可能两个节点相互引用(也就是两个节点的next指针都指向对方),这样就形成了一个环形的数据结构,最终造成死循环。但在JDK1.8中,HashMap在扩容中采用了尾插法,这样就避免了在扩容中引起死循环,但是这不意味着在JDK1.8中多线程就可以使用HashMap了,因为HashMap还会引起数据可见性等其他问题。在多线程下,我们还是得使用HashTable或者concurrentHashMap。

concurrentHashMap

在介绍concurrentHashMap之前,先来简单了解HashTable,HashTable读和写都使用了synchronized来保证线程安全,在线程竞争非常激烈的环境下,HashTable的效率十分低下。所以我们大部分多线程场景下还是使用concurrentHashMap来保证线程安全,但是这也不代表着concurrentHashMap就可以完全代替HashTable了,因为concurrentHashMap是弱一致性的,无法保证一个线程刚put进一个数据,其他线程就马上能get到这个数据,而HashTable是强一致性的,这在后面会详细讲一讲。
我们在开头说过在JDK1.8中,concurrentHashMap有很大的改动并且在结构上与HashMap非常的相似。在JDK1.7以前,concurrentHashMap采用了分段锁的思想,即是采用了segment数组,每个segment数组中又有很多的Entry,在操作concurrentHashMap只需对segment加锁。

并且Entry的vaule和segment的count都是volatile的,所以get操作是无需加锁的,在使用size()获得concurrentMap的长度时也非常方便,直接对每个count相加就行了,但是如果此时又有新的数据put进来了怎么办,所以concurrentHashMap有一个modCount,当进行put、remove都会计入modCount。所以在计算size时,会先尝试两次不加锁的进行对count相加,如果modCount两次都改变了,那么就会对put、remove加锁进行size计算操作。
在JDK1.8之后,concurrentHashMap抛弃了segment,而改用了数组+链表+红黑树(和HashMap类似),并且使用cas和synchronized保证线程安全,因此从之前的对segment加锁变为了对数组节点加锁。

为什么concurrentHashMap是弱一致性的?

我们在开头说过concurrentHashMap是弱一致性的,而要求强一致性的场景下尽量使用HashTable,这是为什么呢,在coucurrentHashMap中,虽然put方法是synchronized的,但是get方法并没有加锁,并且在concurrentHashMap的node中只有next和vaule是volatile的,如果在一个链表(或者红黑树)中,节点的下一个节点为空,尾插节点并不是同步的,put方法的尾插节点和get并不能根据happens-before推算出来尾插hb get,所以当一个线程插入一个节点后,此时其他线程并不能马上get到这个节点。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值