并发——ConcurrentHashMap

前言
为什么会有ConcurrentHashMap?
我们知道HashMap在单线程情况下么有问题,但是在多线程情况下,在put的时候,如果插入的元素超过了容量(由负载因子决定)的允许范围就会触发扩容操作,就是rehash。将旧数组的内容重新hash到的新的数组中,因为是多线程的环境中,存在着其他的线程也在进行put操作,如果哈希值相同,就出现了两个线程在同一个数组下用链表表示,造成闭环,从而导致get时出现死循环,所以HashMap线程不安全。HashMap死锁原因分析

那再来说说HashTable,它是线程安全的,因为它所涉及的多线程操作都加上了synchronized关键字来锁住整个table,这样的话在多线程环境中,所有线程都在竞争同一把锁,是安全了,但是它是效率低下的。

HashTable有很大优化空间,锁住整个table太粗暴,那就segment进行分段锁(这样温柔)。多线程环境中,不同的数据集进行操作其实不用去竞争一个锁,因为他们hash值不同,不会rehash造成线程不安全(HashMap不安全不就是因为hash可能一样,并发操作同一个数组下的链表吗),他们互不影响,实现了锁分离技术,将锁的细化程度增高,粒度降低,利用多个锁来控制多个小的table

优化后就出现了JDK1.7的ConcurrentHashMap

在JDK1.7版本中,ConcurrentHashMap的数据结构采用Segment数组(分段数组)加HashEntry组成
在这里插入图片描述在这里插入图片描述
在这里插入图片描述Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样

Segment:分段锁

在HashMap中,是使用一个哈希表,即元素为链表结点Node组成的数组table,而在ConcurrentHashMap中是使用
多个哈希表,具体为通过定义一个Segment来封装这个哈希表其中Segment继承于ReentrantLock,故自带lock的功
能。即每个Segment其实就是相当于一个HashMap,只是结合使用了ReentrantLock来进行并发控制,实现线程安全


Segment定义如下:ConcurrentHashMap的一个静态内部类,继承于ReentrantLock,在内部定义了一个HashEntry
数组table,HashEntry是链表的节点定义,其中table使用volatile修饰,保证某个线程对table进行新增链表节点(头
结点或者在已经存在的链表新增一个节点)对其他线程可见。

初始化
ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小,用size来表示`

int size =1;
while(size < concurrencyLevel) {
++a;
size <<=1;
}

如上所示,因为size有位于运算来计算,所以Segment的大小取值都是2的N次方,于concurrencyLevel的取值无关,但是concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最大是65536个。若无指定的concurrencyLevel元素初始化,Segment的的大小size默认是16。

put操作

static class  Segment<K,V> extends  ReentrantLock implements  Serializable {
}

从上Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒

  • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  • 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  • 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  • 最后会解除在 1 中所获取当前 Segment 的锁。

get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null

JDK1.8的ConcurrentHashMap
1.7已经解决了并发的问题,并且能支持N个segment多次数的并发,但依然存在HashMap的1.7版本的问题。**链表的查询遍历链表效率太低 **
因此1.8中做了底层数据结构的修改:
在这里插入图片描述

抛弃segment分段锁,而是采用CAS+synchronized来保证并发的安全性。同时将1.7中的HashEntry改成Node,但是作用是相同的,其中val和next 用volatile修饰,保证了可见性。
在这里插入图片描述
put方法

  • 根据key计算出HashCode
int hash = spread(key.hashCode());
  • 判断是否需要进行初始化
if (tab == null || (n = tab.length) == 0)
                tab = initTable();
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
		else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                    break;                   // no lock when adding to empty bin
            }
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
else if ((fh = f.hash) == MOVED)
  • 如果都不满足,则利用 synchronized 锁写入数据。
synchronized (f) {
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
			   if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }

get方法

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
int h = spread(key.hashCode());
  • 如果是红黑树那就按照树的方式获取值。 就不满足那就按照链表的方式遍历获取值。
if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }

总结

看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。

其实这块也是面试的重点内容,通常的套路是:

  • 谈谈你理解的 HashMap,讲讲其中的 get put 过程。
  • 1.8 做了什么优化?
  • 是线程安全的嘛?
  • 不安全会导致哪些问题?
  • 如何解决?有没有线程安全的并发容器?
  • ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?

这一串问题相信大家仔细看完都能怼回面试官。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值