并发容器之ConcurrentHashMap

并发容器之ConcurrentHashMap

前言

鉴于HashMap在多线程下的安全问题,所以JDK在1.5版本引入了线程安全的HashMap,即是本文将要说的ConcurrentHashMap,他在1.8采用了降低锁的粒度的思想,引入段Segment的概念,然而在1.8后又取消了这个概念

Before JDK 1.8

数据结构

这里写图片描述
Segment数组+链表,每个Segment包含一个与HashMap数据结构差不多的链表数组,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型

 static final class HashEntry<K,V> {
        final int hash;                 //确保唯一性
        final K key;                    //确保唯一性
        volatile V value;
        final HashEntry<K,V> next;
        //其他省略
}

确定Segment位置

ssize表示Segment的大小,一定是大于或等于concurrentLevel的最小的2的次幂,2的sshift次方等于ssize,取Segment桶的位置的时候则会保留sshift位高位(通过segmentShift=32-sshift,右移)来与Segment长度减一(segmentMask:段掩码)相与获得桶的位置

Segment内确定具体桶的位置

和原HashMap的思想一致

同步方式

Segment继承了ReentrantLock,具备锁的能力,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑

扩容机制

rehash方法中会定位第一个后续所有节点在扩容后index都保持不变的节点,然后将这个节点之前的所有节点重排即可

求size

为更好支持并发操作,ConcurrentHashMap会在不上锁的前提逐个Segment计算3次size,如果某相邻两次计算获取的所有Segment的更新次数(每个Segment都与HashMap一样通过modCount跟踪自己的修改次数,Segment每修改一次其modCount加一)相等,说明这两次计算过程中无更新操作,则这两次计算出的总size相等,可直接作为最终结果返回。如果这三次计算过程中Map有更新,则对所有Segment加锁重新计算Size。

After JDK 1.8

数据结构

放弃了分段锁的方案,而是直接使用一个大的数组

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;           //它的Key值和hash值都由final修饰,不可变更
  final K key;
  volatile V val;           //Value及对下一个元素的引用由volatile修饰,可见性也有保障
  volatile Node<K,V> next;
}

//当链表长度过长的时候,会转换为TreeNode
//由TreeBin完成对红黑树的包装,替了TreeNode的根节点

重要属性

  1. sizeCtl:在不同的地方有不同用途,而且它的取值不同,也代表不同的含义
    • 负数代表正在进行初始化或扩容操作
    • -1代表正在初始化(初始化只能由一个线程完成)
    • -N 表示有N-1个线程正在进行扩容操作
    • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
  2. ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用,只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动

同步方式

其中大量用到了CAS,对于put操作的话,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值。如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素(也即链表表头或者树的根元素)使用synchronized关键字申请锁,然后进行操作。如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率。

如果f的hash值为-1,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作

扩容机制

  • 单线程内扩容:
    1. 构建一个nextTable,它的容量是原来的两倍
    2. 把table的数据复制到nextTable中
      1. 首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd
      2. 如果f == null,则在table中的i位置放入fwd
      3. 如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
      4. 如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上
  • 多线程扩容
    如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容。
    多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。这样交叉就完成了复制工作。

求size

通过baseCount来持有这个size,因为进行一次put操作后都会进行更新这个值,求个数的方法会在mappingCount或size方法返回,两个方法都没有直接返回basecount 而是统计一次这个值,而这个值其实也是一个大概的数值,因此可能在统计的时候有其他线程正在执行插入或删除操作

与HashMap 的不同

  1. ConcurrentHashMap线程安全,而HashMap非线程安全。
  2. HashMap允许Key和Value为null,而ConcurrentHashMap不允许。
    因为concurrenthashmap它们是用于多线程的,并发的 ,如果map.get(key)得到了null,不能判断到底是映射的value是null,还是因为没有找到对应的key而为空,而用于单线程状态的hashmap却可以用containKey(key) 去判断到底是否包含了这个null。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值