ConcurrentHashMap

我们知道,HashMap是线程不安全的,而其它两种HashTable和Collections.synchronizedMap性能又很差,因此在这种并发环境下,为了能够兼顾线程安全以及执行效率,ConcurrentHashMap就应运而出了

ps:如对上述Map知识有所不了解,可以点击此处链接
HashMap小结

ConcurrentHashMap1.7中主要功能的实现

概念及背景

ConcurrentHashMap 其底层可以看做一个二级的 HashMap,第一层存储的是 Segment 数组对象,每个 Segment 存储着一个 HashEntry 数组,其下存储着很多 key-value 键值对

在这里插入图片描述
在这里插入图片描述
ConcurrentHashMap 此种设计采用分治的方法,让每个 Segment 实现读写的高度自治,如此一来就能很大程度上减少锁的使用基础上还能保证安全

并发环境下的ConcurrentHashMap:

  • 不同的 Segment 在两个线程同时进行读写操作,因为在不同 Segment 中不会有线程安全问题
    在这里插入图片描述

  • 同一 Segment 下两个线程同时进行读操作,因为没有进行数据的修改所以无需上锁,没有安全问题
    在这里插入图片描述

  • 同一 Segment 下两线程同时进行读写操作,写操作会有数据的修改,两个线程不能同时进行
    在这里插入图片描述
    Segment的写操作有加同步锁,对同一个线程进行写操作获取不到锁时,会进入阻塞态
    如此一来,每个Segment各持一把锁,这样就能最大程度的增大效率并且保证了线程安全

get方法

  1. ConcurrentHashMap通过对 key 进行hash运算
  2. 通过得到的hash值确定在哪个 Segment对象下
  3. 再通过hash值,确定在Segment数组的具体哪个位置
  4. 最后返回该节点的value

put方法

  1. ConcurrentHashMap通过对 key 进行hash运算
  2. 通过得到的hash值确定在哪个 Segment对象下
  3. 获取锁,若获取到进入第4步,否则阻塞等待
  4. 再通过hash值,确定在Segment数组的具体哪个位置
  5. 对要修改的HashEntry进行操作
  6. 释放锁

Size方法

ConcurrentHashMap的size()方法是一个循环嵌套方法

  1. 遍历全部Segment
  2. 给Segment的元素数量求和
  3. 给Segment的修改次数求和
  4. 将计算得的修改次数之和与上次的和将比较,若增加,说明在计算中有数据的修改,则重新统计,并且定义一个尝试次数,对其++
  5. 若尝试次数大到一个阈值,则对Segment对象加锁,再做统计
  6. 统计结束释放锁,返回统计的size

其size的加锁操作为乐观锁的方式,即先假设在统计次数过程中不会有数据的修改,如果最后发现则重新统计,如果重复多次依旧不安全,则为该Segment加锁

ConcurrentHashMap1.8中主要功能的实现

简单认识

  • 与 jdk1.7 不同,ConcurrentHashMap 没有用到 Segment创建一个二级HashMap,而是沿用了HashMap的底层 散列表+红黑树实现
  • ConcurrentHashMap是线程安全的,支持多线程环境下的并发读写
  • 查找操作即get方法并没有加锁,因为其操作不会影响线程安全
  • ConcurrentHashMap中的key 和 value都不允许为空

较jdk1.7的改进

  • 取消了 Segment 的设计,取而代之的是直接的Node对象,使用Node数组来存储数据,并对每个数组中的元素考虑进行加锁
    在这里插入图片描述
  • 底层引入红黑树,当Node元素下的链表长度大于8时,该链表由于过长,查询元素时效率较低(O(n))于是就将其转化为红黑树,从而提高查找效率
    在这里插入图片描述
  • 取消jdk1.7的Segment分段锁机制,改为CAS+Synchronized实现线程安全

get方法

  1. 获取hash值,通过hash值确定在散列表的哪个位置(Node)
  2. 若key == node.val,则直接返回node
  3. 如果此时在扩容则调用该节点的find()方法,查找到就返回结点否则返回null
  4. 如果key == node.val,则遍历节点找它的下一个,直到全部遍历完还没找到则返回null

在这里插入图片描述

put方法

  • 首先进入自旋过程,直到抢占到所该线程put成功
  • 如果数组没有初始化,先进行初始化操作吊桶initTable()方法
  • 如果没有发生哈希冲突,就调用casTabAt()方法,执行CAS操作
  • 此时如果有线程正在执行扩容操作,则扩容操作先一级进行
  • 如果发现哈希冲突,就去抢占锁,当链表时直接尾插,当为红黑树时按其树结构插入
  • 如果插入前是链表,插入结束后链表长度大于8,则将链表转化为红黑树
  • 如最后添加成功则调用addCount()方法统计size,检查是否需要扩容
    在这里插入图片描述

获取size方法

jdk1.8获取 size 是通过维护两个变量,对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size

  • baseConnt:为记录节点个数的变量,volatile修饰
  • counterCells:其本质是一个数组,其内部存放着counterCell,每个 counterCell 存放着部分节点个数,counterCell类其内部只有一个 volatile 修饰的变量,用于存放结点个数,该类被@sun.misc.Contended 这个注解标识着这个类防止需要防止 “伪共享”。

伪共享:缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能

每次put 和 remove 操作中,如果操作成功最后方法返回前都会调用addCount()方法,此方法会根据传入的参数更新baseConnt 与 counterCells,从而实时记录节点个数

  • List item

具体addCount()操作:

  1. 当 counterCells == null 时,尝试对baseCount进行CAS,如果成功则完成更新,否则尝试对counterCells进行CAS,执行fullAddCount()
  2. 线程通过随机数ThreadLocalRandom.getProbe() & (n-1) 计算出在counterCells数组的位置,如果不为null,则fullAddCount()一直循环直到成功,否则counterCells数组会进行扩容为原来的两倍,继续随机,继续添加
    在这里插入图片描述

具体的size()操作:

  • 通过 sumConut() 获取当前的size
    在这里插入图片描述

  • 进入 sumCount() 方法,迭代counterCells,将其与baseConnt加和最终统计
    在这里插入图片描述

  • 因为 size() 最终返回的是int类型,而Map的size有可能超过int定义的最大值Integer.MAX_VALUE,当大于最大值是,返回 Integer.MAX_VALUE,因此jdk中获取size的方法推荐使用mappingCount(),其与size()唯一的区别是,返回值为long类型,无序考虑size的大小
    在这里插入图片描述

ConcurrentHashMap 在 jdk1.7 与 jdk1.8的不同:

  • 底层实现不同:1.8中取消了 Segment 的二级HashMap结构,而是使用散列表+链表/红黑树来实现
  • 线程安全机制不同:1.7中使用 Segment 分段锁机制,1.8中使用 CAS+Synchronized 实现线程安全
  • 锁的粒度不同:1.7中是对 每个Segment对象加速,1.8中对每个元素,即Node结点加锁
  • 哈希冲突时存放相同hash值元素的底层数据结构不同,1.8在1.7链表储存的基础上,进行优化当链表长度超过阈值8时,链表会转化为红黑树,从而提高查询效率

HashTable、HashMap、ConcurrentHashMap的区别

在这里插入图片描述

  • HashMap允许可以有一个key和多个value为null,HashTable和ConcurrentHashMap不允许
  • HashMap线程不安全,HashTable与ConcurrentHashMap是线程安全的
  • 效率:因为三者单线程理论上操作的时间复杂度都为O(1),总体上
    HashMap由于不保证线程安全,所以效率最高
    HashTable直接使用Synchronized修饰方法实现线程安全,但每次操作都会将HashTable整体锁住,执行效率很低
    ConcurrentHashMap使用CAS+Synchronized实现线程安全,在方法内部对部分代码块使用Synchronized,操作时先尝试CAS若多次尝试仍失败,则进入Synchronized
  • 底层数组扩容:
    HashTable初始大小为11,扩容方式为 new = old*2+1
    HashMap和ConcurrentHashMap相同,初始值都为16,扩容方式为
    new = old²

以上便是对ConcurrentHashMap的知识点小结,随着后续学习的深入还会同步的对内容进行补充和修改,如能帮助到各位博友将不胜荣幸,敬请斧正

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值