ConcurrentHashMap

ConcurrentHashMap

1.ConcurrentHashMap的出现

我们最常用的集合框架一定包括HashMap,但是都知道它不是线程安全的。在并发插入元素的时候,有可能出现带环链表,让下一次读操作出现死循环。

image-20220113103410159

而想要次避免HashMap的线程安全问题有很多办法,比如改用HashTable或者Collections.synchronizedMap。但是,这两者有着共同的问题:性能。无论读操作还是写操作,它们都会给整个集合加锁,导致同一时间的其他操作为之阻塞。

image-20220113103434442

image-20220113103442881

因此,ConcurrentHashMap应运而生。

2.ConcurrentHashMap底层结构

2.1.JDK1.7之前

在了解ConcurrentHashMap之前,首先要了解一个概念Segment。Segment本身就相当于一个HashMap对象。

Segment事实上是一个可重入锁,它继承了Reentrantlock.

同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头结点。

单一的Segment结构如下:

image-20220113141709352

像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。

在这里插入图片描述
因此整个ConcurrentHashMap的结构如下:

image-20220113141842110

可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个自哈希表。

使用这样锁分段技术,每一个Segment就好比一个自治区,读写操作高度自治,Segment之间互不影响。

情况1:不同Segment的并发写入

image-20220113143254395

不同Segment的写入是可以并发执行的。

情况2:同一Segment的一写一读

image-20220113143349995

同一Segment的写和读是可以并发执行的。

情况3:同一Segment的并发写入

image-20220113143438318

Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。

由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

我们可以看一下ConcurrentHashMap的读写过程:

Get方法

  1. 为输入的Key做hash运算,得到hash值。
  2. 通过hash值,定位到对应的segment对象。
  3. 再次通过hash值,定位到segment当中的数组的具体位置。

Put方法

  1. 为输入的key做hash运算,得到hash值。
  2. 通过hash值,定位到对应的segment对象。
  3. 获取可重入锁。
  4. 再次通过hash值,定位到segment当中数组的具体位置。
  5. 插入或覆盖hashEntry对象。
  6. 释放锁。

可以看出ConcurrentHashMap在读写时都需要二次定位。首先定位到segment,之后定位到segment内的具体数组下标。

在调用size方法的时候,如何解决一致性的问题?

size方法的目的是统计ConcurrentHashMap的总元素数量,自然需要把各个segment内部的元素数量汇总起来。

但是,如果在统计segment元素数量的过程中,已统计过的segment瞬间插入新的元素,这时候该怎么办呢?

image-20220113144351140

ConcurrentHashMap的size方法是一个嵌套循环,大体逻辑如下:

  1. 遍历所有的segment。
  2. 把segment的元素数量累加起来。
  3. 把segment的修改次数累加起来。
  4. 判断所有segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
  5. 如果尝试次数超过阈值,则对每一个segment加锁,再重新统计。
  6. 再次判断所有segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
  7. 释放锁,统计结束。

可以看看源码:

public int size() {
   
    // Try a few times to get accurate count. On failure due to
   // continuous async changes in table, resort to locking.
   final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
   
        for (;;) {
   
            if (retries++ == RETRIES_BEFORE_LOCK) {
    // 如果超过阈值,则进行加锁计算
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum 
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值