JDK8HashMap和ConcurrentHashMap源码剖析

前面分析我们知道jdk7中HashMap是通过数组+链表的结构实现的,而jdk8中HashMap多了一个红黑树的实现,在一定程度上优化查询和插入元素。

HashMap

初始化:new HashMap() 仅仅只是初始化了负载因子

put()方法

判断table属性(Node数组对象)是否为null或者数组长度为0,是则调用resize()方法初始化HashMap的数组长度16,阈值12。通过(length-1)&hash计算数组下标;判断当前数组下标是否为空,是则创建Node对象并放入table数组中;

以上都不满足,则说明数组下标对应的位置有对象,判断是否有hash冲突而且key值相等,则覆盖当前key值所对应的value值;如果不相等则判断当前数组下标对应的对象是否为TreeNode类型,也就是该位置是对应红黑树,则将该key插入到红黑树,如果有冲突并且key值相等也会覆盖;以上都不满足,则需要将该元素使用尾插法插入到链表中,有冲突也会覆盖,这里还会判断链表长度是否大于8,目的是将链表改为红黑树来存储(大于8则数化)。最后modCount++,如果size大于阈值则扩容

扩容

数组大小扩大两倍,阈值扩大两倍,如果当前数组位置只有一个元素直接通过hash&(newCap-1)计算新的下标并放入新的数组中;如果当前位置对象存储的是TreeNode,则通过hash&oldCap,因为数组容量为2的次方,所有结果要么是0要么是1,最后将所有节点分为2组并统计各种元素数量,如果2组有小于6个元素的,则进行红黑树转链表(遍历所有节点,创建新节点,放入对应数组下标的位置);否则当前位置为链表,跟红黑树一样遍历链表对链表进行分为2组,并放入对应新数组的下标位置。

树化(红黑树转链表)

遍历链表中所有的节点,创建TreeNode对象并初始化prev和next属性组成双向链表,遍历新的双向链表,以第一个节点作为root开始创建红黑树,第二个节点与第一个节点进行比较(比较的顺序:hash值,class对象,compareTo()方法,System.identityHashCode(obj)),小于则为左节点,大于则为右节点,最后调用balanceInsertion()方法进行调整直到满足红黑树的定义为止,最后将root节点引用赋值给当前数组下标引用。

get()方法

同样,计算key的hash值,hash&(length-1)算出数组下标,获取该下标的第一个元素进行进行比较,如果hash相同而且key值相等则返回,否则判断第一个元素是否为TreeNode对象,是则遍历红黑树查找对应的value,如果不是则遍历链表。

ConcurrentHashMap

JDK7中ConcurrentHashMap是通过unsafe操作+ReentrantLock+分段锁思想实现线程安全的

JDK8中ConcurrentHashMap的线程安全实现是靠unsafe+synchronized,目的是保证数组某个位置引用赋值和插入红黑树或者链表的操作唯一,unsafe操作:

        compareAndSwapObject():通过cas操作修改对象的属性

        putOrderdObject():并发安全的给数组的某个位置赋值

        getObjectVolatile():并发安全的获取数组中某个位置的元素

put()方法

计算hash值,第一次进for循环table属性为空,进而通过unsafe的cas操作保证初始化ConcurrentHashMap的table和sizeCtrl只有一个线程执行;进行循环通过(length-1)&hash计算数组下标,判断当前下标位置是否为空,是则通过调用compareAndSwapObject()方法向数组某位置中赋值;如果不为空判断hash是否为-1,为-1表示其他线程正在扩容,则该线程回去帮助扩容转移元素;否则加synchronized锁(对象为当前数组下标所对应的对象)如果该槽位的hash值大于0则存储的是链表那么遍历链表进行尾插法插入元素并计数(用来判断是否需要树化),如果当前对象属于TreeBin(包装了TreeNode类型的root对象)则是红黑树,相关插入算法和HashMap一样。判断链表的数量是否大于等于8,是则加锁(synchronized)遍历链表为每一个节点重新创建TreeNode对象并生成双向链表,然后创建TreeBin对象构建红黑树并将数组引用指向该TreeBin对象。最后调用addCount()方法,该方法可以分为两个大步骤:(1)+1计数操作;(2)扩容元素转移操作

addCount() +1操作

在jdk8中ConcurrentHashMap计算容器中元素个数是通过baseCount和CounterCell[]实现。

从获取所有元素个数的sumCount()方法可以看出,元素个数等于baseCount加上所有CounterCell数组的value值。

final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

 接下来看看addCount()方法怎么操作baseCount和CounterCell数组是+1操作的

1、先cas操作对baseCount+1,多线程情况下,只有一个能成功,其他线程均失败

2、失败的线程会判断当前ConcurrentHashMap的counterCells数组是否为null,为null则调用fullAddCount(x, uncontended)进行初始化并加一操作,不为空则以当前线程作为参数生成一个随机数和counterCells数组大小进行与操作,得到数组下标,然后对counterCells的该位置进行cas+1操作,操作成功则继续判断是否需要扩容,失败则调用fullAddCount()方法确保这次+1操作成功。

3、fullAddCount()方法大体可以分为三个执行逻辑:(1)cas操作对counterCells的某一个CounterCell对象的value+1以及是否需要对counterCells扩容,扩容的前提是一个线程两次循环都没有+1成功而且数组大小小于NCPU(我猜应该是cpu核数);(2)初始化counterCells;(3)cas对baseCount+1。在多线程执行时,首先counterCells数组是为空的,cas操作会保证只有一个线程进行counterCells的初始化,其他线程尝试cas对baseCount进行加1操作,如果此时某一线程countCells初始化完成,对baseCount+1失败的线程就会走到(1)流程

CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;

addCount() 扩容(转移)

当执行完cas+1操作后,会对容器中元素个数进行统计,如果大于初始化中设置的阈值,则通过cas操作确保只有一个线程t1会进行扩容;t1创建大小为原来两倍的新数组,拿到最小的步长16(这个是t1要转移的槽的个数),例如,当前ConcurrentHashMap大小为32,那么t1会从31的槽位开始进行转移元素直到转移了16个槽位结束,每转移一个槽位会将旧数组的槽位元素hash值置为-1;如果此时t1转移元素还没有结束,有另外一个线程t2调用put方法发现该槽位hash值为-1,则会帮忙转移元素,t2会从15的槽位开始遍历直到0结束。具体的链表和红黑树元素转移流程和上面的HashMap一样不再赘述。

get()方法

(1)计算hash值并得到数组下标

(2)判断当前下标位置的对象的hash值和key值是否与传入的key相同,是直接返回

(3)否则判断当前下标位置的对象的hash值是否小于0,则表示红黑树(默认为TreeBin对象,hash值为-2),通过节点的next属性遍历红黑树,找到目标值

(4)以上都不是表示是链表,则遍历查找

remove()方法

(1)计算hash值并得到数组下标

(2)判断table是否为null,目标位置的槽是否为null,是直接返回null

(3)如果该槽位的hash值等于-1,表示其他线程在扩容,则帮助扩容

(4)判断hash值是否大于0,大于直接遍历,找到目标值,进行剔除

(5)最后判断是否属于TreeBin类型,进行剔除并判断是否需要红黑树转链表操作。

(6)如果有元素被剔除,调用addCount()方法做减一操作。

总结

JDK7和JDK8的HashMap比较

相同点

  • 都是线程不安全的
  • key和value都可以为空。
  • 都会出现死循环问题,JDK8中红黑树依然会有死循环问题

不同点

  • JDK7多线程头插法扩容会导致死循环,JDK8红黑树死循环。
  • JDK7中数组+链表结构,JDK8中使用数组+链表+红黑树数据结构、
  • JDK7扩容使用头插法,JDK8使用尾插法将数据分为两组放入新的数组中,注意当元素小于6时红黑树需要转链表
  • JDK8在put时会判断链表长度是否大于8,是则会将链表转为红黑树

ConcurrentHashMap比较

 相同点

  • 都是线程安全的
  • key和value都不能为空

不同点

  • JDK7通过Segment(实现)数组和HashEntry数组结构实现;JDK8使用Node数组(链表),红黑树使用TreeNode
  • JDK7通过unsafe操作(cas)+ReentrantLock+分段锁思想实现线程安全,JDK8通过unsafe+synchronized保证线程安全
  • JDK7累加每个Segment维护的count总和,JDK8通过baseCount和CounterCell[]并发实现元素个数统计
  • 扩容:JDK7只会判断某个Segment对象是否满足扩容条件,满足则对该Segment对象下的HashEntry数组进行两倍扩容,并且扩容的过程是加锁的;JDK8在put时发现元素个数超过阈值,则会扩容某一段(步长为16),如果此时另一个线程发现有线程正在扩容,则会帮助一起扩容。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值