暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ConcurrentHashMap原理

程序员葫芦 2021-11-15
270

数据结构:

   在JDK1.7中

     ConcurrentHashMap是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),

其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个HashEntry对象链接起来的链表。

特别地,ConcurrentHashMap在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个Segment大约守护整个散列表中桶总数的1/16。

  在JDK1.8中

   ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并发控制使synchronized和CAS来操作。

   也将1.7中存放数据的HashEntry改为Node,但作用都是相同的。其中的 val next 都用了volatile修饰,保证了可见性。

  属性字段:

        // 用于定位段,大小等于segments数组的大小减 1,是不可变的

        final int segmentMask; 

        // 用于定位段,大小等于32(hash值的位数)减去对segments的大小取以2为底的对数值,是不可变的

        final int segmentShift;

// ConcurrentHashMap的底层结构是一个Segment数组

final Segment<K,V>[] segments;   

Segment:Segment类继承于ReentrantLock类,从而使得Segment对象能充当锁的角色。

   每个Segment对象用来守护它的成员对象table中包含的若干个桶。table是一个由HashEntry对象组成的链表数组,table数组的每一个数组成员就是一个桶。

   ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),而ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分。

     Segment中的字段及含义:

 // Segment中元素的数量,可见的

  transient volatile int count; 

 //对count的大小造成影响的操作的次数(比如put或者remove操作)

          transient int modCount; 

 // 阈值,段中元素的数量超过这个值就会对Segment进行扩容

 transient int threshold;

  // 链表数组

           transient volatile HashEntry<K,V>[] table;

  // 段的负载因子,其值等同于ConcurrentHashMap的负载因子

  final float loadFactor;   

  基本元素:HashEntry

    HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。

  ConcurrentHashMap构造函数

  1、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

  ---该构造函数意在构造一个具有指定容量、指定负载因子和指定段数目/并发级别(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap

      2、ConcurrentHashMap(int initialCapacity, float loadFactor)

      ---该构造函数意在构造一个具有指定容量、指定负载因子和默认并发级别(16)的空ConcurrentHashMap

      3、ConcurrentHashMap(int initialCapacity)

  ---该构造函数意在构造一个具有指定容量、默认负载因子(0.75)和默认并发级别(16)的空ConcurrentHashMap

       4、ConcurrentHashMap()

  ---该构造函数意在构造一个具有默认初始容量(16)、默认负载因子(0.75)和默认并发级别(16)的空ConcurrentHashMap

       5、ConcurrentHashMap(Map<? extends K, ? extends V> m)

    ---该构造函数意在构造一个与指定 Map 具有相同映射的 ConcurrentHashMap,其初始容量不小于 16 (具体依赖于指定Map的大小),负载因子是 0.75,并发级别是 16, 是 Java Collection Framework 规范推荐提供的

以上构造函数中有三个重要参数:initialCapacity(初始容量)、loadFactor(负载因子)和concurrencyLevel(并发级别),这三个参数进行构造并初始化segments数组、段偏移量segmentShift、段掩码segmentMask和每个segment的

  put方法:

    jdk1.7中:

  1、尝试自旋获取锁。

  2、如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

      3、将当前Segment中的table通过key的hashcode定位到HashEntry。

  4、遍历该 HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value。

  5、不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容。

      6、最后会解除在1中所获取当前Segment的锁。

     jdk1.8中:

      1、根据key计算出hashcode。

       2、判断是否需要进行初始化。

      3、f 即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。

4、如果当前位置的hashcode == MOVED == -1,则需要进行扩容。

5、如果都不满足,则利用synchronized锁写入数据。

6、如果数量大于TREEIFY_THRESHOLD则要转换为红黑树。

 get方法:

    jdk1.7中:

     只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于HashEntry中的value属性是用volatile关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

      ConcurrentHashMap的get方法是非常高效的,因为整个过程都不需要加锁。

      jdk1.8中:

1、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。

          2、如果是红黑树那就按照树的方式获取值。

          3、就不满足那就按照链表的方式遍历获取值。

    rehash方法:

   ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。

   由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。

   如果元素的hashcode的第k+1位是0,那么元素在新桶的序号就是和原桶的序号是相等的;如果第k+1位的值是1,那么元素在新桶的序号就是原桶的序号加上2^k。

ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。

  ConcurrentHashMap的扩容机制

1.7版本

  1、1.7版本的ConcurrentHashMap是基于Segment分段实现的

  2、每个Segment相对于一个小型的HashMap

  3、每个Segment内部会进行扩容,和HashMap的扩容逻辑类似

  4、先生成新的数组,然后转移元素到新数组中

  5、扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

    1.8版本

  1、1.8版本的ConcurrentHashMap不再基于Segment实现

  2、当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容

  3、如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容

  4、ConcurrentHashMap是支持多个线程同时扩容的

  5、扩容之前也先生成一个新的数组

  6、在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作


文章转载自程序员葫芦,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论