ConcurrentHashMap JDK1.7 和 JDK1.8 的区别

转自:https://www.jianshu.com/p/933289f27270

ConcurrentHashMap 1.8 相比 1.7的话,主要改变为:

  • 去除 Segment + HashEntry + Unsafe 的实现,
    改为 Synchronized + CAS + Node + Unsafe 的实现
    其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。
    用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了再加锁。

  • put()方法中,初始化数组大小时,1.8不用加锁,因为用了个 sizeCtl 变量,将这个变量置为-1,就表明table正在初始化。

下面简单介绍下主要的几个方法的一些区别:

1. put() 方法

JDK1.7中的实现:

ConcurrentHashMap 和 HashMap 的put()方法实现基本类似,所以主要讲一下为了实现并发性,ConcurrentHashMap 1.7 有了什么改变

  • 需要定位 2 次 (segments[i],segment中的table[i])
    由于引入segment的概念,所以需要

  1. 先通过key的 rehash值的高位segments数组大小-1 相与得到在 segments中的位置
  2. 然后在通过 key的rehash值table数组大小-1 相与得到在table中的位置
  • 没获取到 segment锁的线程,没有权力进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:

  1. table[i]的位置(你的值要put到哪个桶中)
  2. 通过首节点first遍历链表找有没有相同key
  3. 在进行1、2的期间还不断自旋获取锁,超过 64次 线程挂起!

JDK1.8中的实现:

  • 先拿到根据 rehash值 定位,拿到table[i]的 首节点first,然后:
  1. 如果为 null ,通过 CAS 的方式把 value put进去
  2. 如果 非null ,并且 first.hash == -1 ,说明其他线程在扩容,参与一起扩容
  3. 如果 非null ,并且 first.hash != -1 ,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。

2. get() 方法

JDK1.7中的实现:

  • get操作实现非常简单和高效。先经过一次再哈希,然后使用这个哈希值通过哈希运算定位到segment,再通过哈希算法定位到元素
  • get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value,所以并不用加锁,valatile保证了共享变量的可见性,所以支持多线程读,但是只支持单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值)
  • 由于变量 value 是由 volatile 修饰的,java内存模型中的 happen before 规则保证了 对于 volatile 修饰的变量始终是 写操作 先于 读操作 的,并且还有 volatile 的 内存可见性 保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。

JDK1.8中的实现:

  • 1.计算hash值,定位到该table索引位置,如果是首节点符合就返回
  • 2.如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点, 匹配就返回
  • 3.以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
  • 和JDK1.7一样,value是用volatile修饰的,所以get操作不用加锁。

3. resize() 方法

JDK1.7中的实现:

  • 跟HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容(1.new个2倍数组 2.遍历old数组节点搬去新数组)。

JDK1.8中的实现:

  • jdk1.8的扩容支持并发迁移节点,从old数组的尾部开始,如果该桶被其他线程处理过了,就创建一个 ForwardingNode 放到该桶的首节点,hash值为-1,其他线程判断hash值为-1后就知道该桶被处理过了。

4. 计算size

JDK1.7中的实现:

  1. 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。
  2. 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的 Count 的和

JDK1.8中的实现:

由于没有segment的概念,所以只需要用一个 baseCount 变量来记录ConcurrentHashMap 当前 节点的个数

  1. 先尝试通过CAS 修改 baseCount
  2. 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将 CELLSBUSY 置1,成功则可以把 baseCount变化的次数 暂存到一个数组 counterCells 里,后续数组 counterCells 的值会加到 baseCount 中。
  3. 如果 CELLSBUSY 置1失败又会反复进行CAS baseCount 和 CAS counterCells数组

相关阅读:谈谈ConcurrentHashMap1.7和1.8的不同实现

1. Segment 数量 在 JDK1.7ConcurrentHashMap 内部的 Segment 数组的长度是固定的,由一个常量指定,即 16。每个 Segment 可以被看作是一个小的 ConcurrentHashMap。在 JDK1.8 中,Segment 被取消了,取而代之的是一个名为 Node 的数组,这个数组的长度是可变的,它的长度被初始化为 2 的幂次方,具体的大小取决于当前 ConcurrentHashMap 中元素的数量。 2. 数据结构 在 JDK1.7ConcurrentHashMap 内部的每个 Segment 由一个 HashEntry 数组和一个 HashEntry 链表组成。当多个线程同时访问 ConcurrentHashMap 时,它们只会锁定对应的 Segment,而不是整个 ConcurrentHashMap。在 JDK1.8 中,每个 Node 是一个单独的元素,它可以包含多个 key-value 对,每个 Node 之间通过链表进行关联。 3. 锁的优化 在 JDK1.8 中,ConcurrentHashMap 中使用了 CAS 操作来进行锁的优化。在 JDK1.7 中,ConcurrentHashMap 中需要使用锁来保证线程安全,这个锁是对每个 Segment 进行的,也就是说,在多线程访问 ConcurrentHashMap 时,每个线程只能同时访问不同的 Segment,这样可以避免锁的竞争,提高了并发性能。 4. 数据存储方式 在 JDK1.7ConcurrentHashMap 中的数据存储方式是数组和链表结合的方式,其中数组是用来存储数据的,链表是用来解决哈希冲突的。而在 JDK1.8ConcurrentHashMap 中的数据存储方式是数组和链表以及红黑树结合的方式,当链表长度大于 8 时,链表会自动转化为红黑树,这样可以提高查找效率。 5. 性能 由于 JDK1.8ConcurrentHashMap 中使用了 CAS 操作来进行锁的优化,因此在并发性能方面有了很大的提升。同时,在数据存储方式方面,JDK1.8ConcurrentHashMap 中使用了红黑树来优化哈希冲突,这样可以提高查找效率。因此,JDK1.8ConcurrentHashMap 的性能比 JDK1.7ConcurrentHashMap 更加出色。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值