ConcurrentHashMap 1.7与1.8的区别

这篇具有很好参考价值的文章主要介绍了ConcurrentHashMap 1.7与1.8的区别。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表

  • 从1.7到1.8版本,由于HashEntry从链表 变成了红黑树所以 concurrentHashMap的时间复杂度从O(n)到O(log(n))
  • HashEntry最小的容量为2
  • Segment的初始化容量是16;
  • HashEntry在1.8中称为Node,链表转红黑树的值是8 ,当Node链表的节点数大于8时Node会自动转化为TreeNode,会转换成红黑树的结构

1.7和1.8区别

JDK1.7版本:ReentrantLock+Segment+HashEntry
JDK1.8版本:synchronized+CAS+HashEntry+红黑树
1.JDK1.8降低锁的粒度,JDK1.7锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry
2.JDK1.8使用红黑树来优化链表
3.JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock

  • 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
  • synchronized之前一直都是重量级的锁,但是后来java官方是对他进`行过升级的,他现在采用的是锁升级的方式去做的。
    针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
    所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

可以看出到了jdk1.8 锁的粒度进一步细化,那么为什么能够进一步细化并且使用synchronized替换1.7中的ReentrantLock,还是因为,synchronized锁升级的优化,使得锁在更细粒度下仍然有更好的表现

构造函数

1.7

public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // 找的2的整数次幂
    int sshift = 0;  //ssize从1变成大于等于concurrencyLevel的2次幂需要左移的次数
    int ssize = 1;  
    //依据给定的concurrencyLevel并行度,找到最适合的segments数组的长度,
    // 该长度为大于concurrencyLevel的最小的2的n次方
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
        
  
    int c = initialCapacity / ssize;
    //如果使用默认参数,也就是initialCapacity是16,concurrencyLevel是16,那么ssize也就是16,c是1,下面c++这句就不会执行
    if (c * ssize < initialCapacity)
        ++c;
    //最后计算出来的c相当于initialCapacity / ssize向上取整
    
     //cap是每个分段锁中HashEntry数组的长度
    int cap = MIN_SEGMENT_TABLE_CAPACITY
    while (cap < c)
        cap <<= 1;
    // 新建segments 数组,初始化 segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}



构造方法分析:
参数:
initialCapacity,代表的是HashEntry[]数组的大小,也就是ConcurrentHashMap的大小。初始化默认为16.
loadFactor,负载因子,在判断扩容的时候用到,默认是0.75
concurrencyLevel,并发级别,代表Segment[]数组的大小,也就是分段锁的个数,默认是16(jdk1.8为了兼容jdk1.7也有这个构造参数,但是功能含义已经不一样了)
我们举一个例子,假如new ConcurrentHashMap(32, 0.75, 16)就是新建了一个ConcurrentHashMap,他的容量是32,分段锁的个数是16,也就是每个Segment里面HashEntry[]数组的长度是2。但是new ConcurrentHashMap()时,每个Segment里面HashEntry[]数组的长度也是2,因为ConcurrentHashMap规定了Segment数组中HashEntry数组的长度是2。
下面这段代码保证了Segment数组的大小是大于concurrencyLevel的最小2的整数次幂(每左移一次都相当于乘上2,直到值大于concurrencyLevel才停止左移),ssize是初始化Segment数组的大小。

	int ssize = 1;
    while (ssize < concurrencyLevel) {
        ssize <<= 1;
    }
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];


Segment数组的大小为2的整数次幂是因为计算hash值的时候采用与运算(key.hashcode & length-1),加快了计算速度,减少了哈希碰撞。
下面这段代码保证了每个HasHashEntry数组的长度也是2的整数次幂。并且数组长度最小为2。如果比2小就取2。

   int cap = MIN_SEGMENT_TABLE_CAPACITY //最小值,常量2
   while (cap < c)
       cap <<= 1;
   new HashEntry[cap];


举例:new ConcurrentHashMap(33, 0.75, 16) ,那么Segment数组的大小是16,因为最后一个参数是16,16正好是2的整数次方,而HashEntry数组的长度:

  int c = initialCapacity / ssize;   // 33/16=2
    
    if (c * ssize < initialCapacity)// 2*16<33
        ++c;                                    //  c=3
    //最后计算出来的c相当于initialCapacity / ssize向上取整
    
     //cap是每个分段锁中HashEntry数组的长度
    int cap = MIN_SEGMENT_TABLE_CAPACITY
    while (cap < c)  //  2<3
        cap <<= 1;   // cap=4


长度为4.

1.8

 // concurrencyLevel,并发线程数 
 public ConcurrentHashMap(int initialCapacity,
                          float loadFactor, int concurrencyLevel) {
     if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
         throw new IllegalArgumentException();
     if (initialCapacity < concurrencyLevel) 
     	 // 初始容量参数小于并发线程数,将初始容量参数赋值为并发线程数
         initialCapacity = concurrencyLevel;
     // 计算数组长度
     long size = (long)(1.0 + (long)initialCapacity / loadFactor);
     int cap = (size >= (long)MAXIMUM_CAPACITY) ?
         MAXIMUM_CAPACITY : tableSizeFor((int)size);
     this.sizeCtl = cap;
 }

1.8中concurrencyLevel的作用仅仅是一个判断,要求initialCapacity大于等于concurrencyLevel

JDK1.7的实现(ReentrantLock+Segment+HashEntry)

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:
ConcurrentHashMap 1.7与1.8的区别

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
Segment的大小ssize默认为 DEFAULT_CONCURRENCY_LEVEL =16

put操作

对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置。
Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置。

get操作

ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null
为什么get不加锁可以保证线程安全
首先获取value,我们要先定位到segment,使用了UNSAFE的getObjectVolatile具有读的volatile语义,也就表示在多线程情况下,我们依旧能获取最新的segment.
获取hashentry[],由于table是每个segment内部的成员变量,使用volatile修饰的,所以我们也能获取最新的table.
然后我们获取具体的hashentry,也时使用了UNSAFE的getObjectVolatile具有读的volatile语义,然后遍历查找返回.
总结:我们发现整个get过程中使用了大量的volatile关键字,其实就是保证了可见性(加锁也可以,但是降低了性能),get只是读取操作,所以我们只需要保证读取的是最新的数据即可.

size操作

计算ConcurrentHashMap的元素大小是一个有趣的问题,因为他是并发操作的,就是在你计算size的时候,他还在并发的插入数据,可能会导致你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案
1、第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的
2、第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回(美团面试官的问题,多个线程下如何确定size)

rehash操作

ConcurrentHashMap的扩容仅仅是和每个Segment中的HashEntry数组的长度有关。但需要扩容时,只扩容当前Segment中的HashEntry数组即可。也就ConcurrentHashMap中的Segment数组在初始化的时候就确定了,后面扩容不会改变这个长度。
相比较HashMap的resize操作,ConcurrentHashMap的rehash原理类似。但是对其做了一定优化,避免让所有节点进行计算操作。
由于扩容是基于2的幂指来操作,假设扩容前某HashEntry对应到Segment中数组的index为i,数组的容量为capacity,那么扩容后该HashEntry对应到新数组中的index只可能为i或者i+capacity,因此大多数HashEntry节点在扩容前后index可以保持部件。基于此,rehash()方法中会定位第一个后续所有节点在扩容后idnex都保持不变的节点,然后将这个节点之前的所有节点重排即可。

JDK1.8的实现(synchronized+CAS+HashEntry+红黑树)

· 取消segments字段
· table数组+单向链表-----> table数组+ 单向链表+ 红黑树【对于链表个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。】
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作。
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据

put操作

  • 如果没有初始化就先调用initTable()方法对其初始化;
  • 对key进行hash计算,求得值没有哈希冲突的话,则利用自旋CAS操作来进行插入数据;
  • 如果存在hash冲突,那么就加synchronized锁来保证线程安全
  • 如果存在扩容,那么就去协助扩容
  • 加完数据之后,再判断是否还需要扩容

get操作

据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果正在扩容,且当前节点已经扩容完成,那么根据ForwardingNode查找扩容后的table上的对应数据
如果是红黑树那就按照树的方式获取值。如果不满足那就按照链表的方式遍历获取值。

size操作

在JDK1.8版本中,对于size的计算,在扩容和addCount()方法就已经有处理了,可以注意一下Put函数,里面就有addCount()函数,早就计算好的,然后你size的时候直接给你。JDK1.7是在调用size()方法才去计算,其实在并发集合中去计算size是没有多大的意义的,因为size是实时在变的,只能计算某一刻的大小,但是某一刻太快了,人的感知是一个时间段,所以并不是很精确

transfer扩容

1.8版本的扩容比较复杂,具体可以看哲这篇的解析ConcurrentHashMap 成员、方法分析
helpTransfer 会在put,remove,get时发现当前槽的头节点为MOVE状态时 也就是已经转换为ForwardingNode,代表当前节点已经转移完毕,整个ConcurrentHashMap还正在扩容,说明整个concurrenHashMap正在扩容。那么进入helpTransfer方法,协助进行扩容,直到扩容完成,那么如果当前需要操作的节点还不是ForwardingNode即还没有完成扩容操作,那么会直接使用源tab,进行操作,对于写操作,也就是说,扩容期间,除了锁住头节点的槽,和已经扩容完成的节点,其他节点依然正常读写。不会因为访问这些节点进入协助扩容!,可见ConcurrentHashMap对锁粒度的控制十分细。文章来源地址https://www.toymoban.com/news/detail-414782.html

到了这里,关于ConcurrentHashMap 1.7与1.8的区别的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 面试题:HashMap线程不安全 ConcurrentHashMap为什么线程安全

    面试的时候先会喊你说说集合,那些集合线程不安全?当你说了 HashMap 线程不安全,面试官可能会进一步询问你是否了解 ConcurrentHashMap ,以及它是如何实现线程安全的。 ArrayList、LinkedList、TreeSet、HashSet、 HashMap 、TreeMap等都是线程不安全的。 HashTable 是线程安全的。 来看个例

    2024年04月23日
    浏览(47)
  • 美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?

    周末的时候,有一位小伙伴提了一些关于 ConcurrentHashMap 的问题,都是他最近面试遇到的。原提问如下(星球原贴地址:https://t.zsxq.com/11jcuezQs ): 整个提问看着非常复杂,其实归纳来说就是两个问题: ConcurrentHashMap 为什么 key 和 value 不能为 null? ConcurrentHashMap 能保证复合操

    2024年02月11日
    浏览(35)
  • 【JAVA】concurrentHashMap和HashTable有什么区别

    🍎 个人博客: 个人主页 🏆 个人专栏: JAVA ⛳️   功不唐捐,玉汝于成 目录 前言 正文 同步性质: 性能: 允许空键值(Allow Nulls): 迭代器(Iterator): 继承关系: 结语  我的其他博客 在Java的集合框架中, ConcurrentHashMap 和 HashTable 都提供了线程安全的哈希表实现,用

    2024年01月16日
    浏览(54)
  • HashMap和HashTable的区别是什么?

    HashMap和HashTable都是常见的哈希表实现,用于存储键值对。它们在功能上类似,但在以下几个方面存在区别: 线程安全性:HashTable是线程安全的,它的所有操作都是同步的,即多线程同时访问HashTable时会自动进行同步处理,从而保证线程安全。而HashMap是非线程安全的,它的操

    2024年02月15日
    浏览(39)
  • 【多线程进阶】信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题

    前言: 大家好,我是 良辰丫 ,今天学习多线程最后一节内容,我们主要去了解信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题,我们需要重点去掌握,💞💞💞 🧑个人主页:良辰针不戳 📖所属专栏:javaEE初阶 🍎励志语句:生活也许会让我们遍体鳞伤,

    2023年04月27日
    浏览(53)
  • Java----Hashmap,LinkedMap和TreeMap三者的区别

            其中map表示的意思为“映射”,HashMap,LinkedMap和TreeMap这三中类都是对Map接口的实现类,在数据插入方面,HashMap是无序插入,LinkedMap是有序插入,而TreeMap会根据键的值进行排序后再进行插入。在运行方面,前两个的运行速度较快,针对Map对象的存储会选择前两者,

    2024年02月13日
    浏览(45)
  • jdk1.7与jdk1.8的HashMap区别2-底层原理区别

    jdk1.7与jdk1.8的HashMap区别1-基本结构与属性对比_ycsdn10的博客-CSDN博客 1.代码数:JDK1.7与JDK1.8相差了一倍的代码 2.方法数:JDK1.7是40个,JDK1.8是51个(只算基本方法)  进行了4次位运算,5次亦或运算  进行了1次位运算,1次亦或运算 由上一章知道,JDK1.7是数组+链表,JDK1.8是数组

    2024年02月13日
    浏览(42)
  • 面试高频知识点:1集合 1.2 ConcurentHashMap是如何实现线程安全的?(1.8之前后区别)

    ConcurrentHashMap(并发哈希表)是Java集合框架中的一种实现Map接口的类,它专为多线程环境设计,以提供更好的性能和线程安全。在理解 ConcurrentHashMap 是如何实现线程安全的时候,我们可以分别探讨 JDK 1.8 之前和之后的实现。 JDK 1.8 之前的实现 在 JDK 1.8 之前,ConcurrentHashMap 主

    2024年01月23日
    浏览(48)
  • 你真的了解HashSet 和HashMap的区别、优缺点、使用场景吗?

      HashSet 和 HashMap 是 Java 集合框架中的两个常用类,它们都用于存储和管理数据,但在使用方式、功能和性能上有很大的区别。 HashSet:  HashSet 是一个基于哈希表的集合,用于存储不重复的元素,它不存储键值对。它实际上是基于 HashMap 实现的,只存储了键,而值都设置为同

    2024年02月05日
    浏览(45)
  • 1.7 运维可以转行什么

    运维(Operations and Maintenance,简称OM)是信息技术领域中的重要职能,负责管理和维护企业的信息技术基础设施。然而,随着技术的不断发展和个人职业发展的需求变化,有些运维人员可能考虑转行到其他领域。运维人员拥有广泛的技术和管理技能,这使得他们在转行时具备一

    2024年02月16日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包