八股文吟诵大师

Java 容器 --- 并发一致性问题(ConcurrentHa

2021-06-02  本文已影响0人  _code_x

并发读写数据一致性保证(Java并发容器)

写在前

业务开发过程,其实就是用户业务数据的处理过程,因而开发的核心任务就是维护数据一致不出错。现实场景中,多个用户会并发读写同一份数据(如秒杀),不加控制会翻车、加了控制则降低并发度,影响性能和用户体验。

如何优雅的进行并发数据控制呢?本质上需要解决两个问题:

让我们看下Java经典的并发容器CopyOnWriteList以及ConcurrentHashMap是如何协调这两个问题的

CopyOnWriteList

读写策略

CopyOnWrite顾名思义即写时复制策略

针对写处理,首先加ReentrantLock锁,然后复制出一份数据副本,对副本进行更改之后,再将数据引用替换为副本数据,完成后释放锁

针对读处理,依赖volatile提供的语义保证,每次读都能读到最新的数组引用

读-写冲突

显然,CopyOnWriteList采用读写分离的思想解决并发读写的冲突

当读操作与写操作同时发生时:

可见在读写分离的设计下,并发读写过程中,读不一定能实时看到最新的数据,也就是所谓的弱一致性。

也正是由于牺牲了强一致性,可以让读操作无锁化,支撑高并发读

写-写冲突

当多个写操作的同时发生时,先拿到锁的先执行,其他线程只能阻塞等到锁的释放

简单粗暴又行之有效,但并发性能相对较差

ConcurrentHashMap(JDK7)

读写策略

主要采用分段锁的思想,降低同时操作一份数据的概率

针对读操作:

针对写操作:

读-写冲突

当读操作与写操作同时发生时:

可见,支持无锁并发读操作还是弱一致的

写-写冲突

ConcurrentHashMap(JDK8)

读写策略

与JDK7相比,少了Segment分段锁这一层,直接操作Node数组(链表头数组),后面称为桶

读-写冲突

当读操作与写操作同时发生时:

因此只要写操作happens-before读操作,volatile语义就可以保证读的数据是最新的,可以说JDK8版本的ConcurrentHashMap是强一致的(此处只关注基本读写(GET/PUT),可能会有弱一致的场景遗漏,例如扩容操作,不过应该是全局加锁的,如有错误烦请指出,共同学习)

写-写冲突

小结

为什么这么设计(个人观点)

对数据进行存储必然涉及数据结构的设计,任何对数据的操作都得基于数据结构,常规思路是对整个数据结构加锁,但是锁的存在会大大影响性能,所以接下来的任务,就是找到哪些可以无锁化的操作。

操作主要分为两大类,读和写。

先看写,因为涉及到原有数据的改动,不加控制肯定会翻车,怎么控制呢?写操作也分两种,一种会改变结构,一种不会

确保数据不会改错之后,读相对就好办了,主要考虑是不是要实时读最新的数据(等待写操作完成),也就是强一致还是弱一致的问题

常见面试问题分析

1.ConcurrentHashMap Hashtable 的区别?

ConcurrentHashMap与HashTable都可以用于多线程的环境(都是线程安全的容器),但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。

(1)底层数据结构:

(2)实现线程安全的方式(重要)

2.ConcurrentHashMap有哪些构造函数?

//无参构造函数
public ConcurrentHashMap() {
}
//可传初始容器大小(initialCapacity)的构造函数
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
//可传入map的构造函数
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}
//可设置阈值(加载因子*容量)和初始容量
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, 1);
}

//可设置初始容量和阈值和并发级别(concurrencyLevel)
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

3.ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?(怎么保证线程安全的?)

JDK1.7

在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

JDK1.8

ConcurrentHashMap用什么技术保证线程安全?

4.ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?ConcurrentHashMap能完全替代HashTable吗?

HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的。

ConcurrentHashMap弱一致性:ConcurrentHashMap可以支持在迭代过程中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程(添加元素等)也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。ConcurrentHashMap的get,clear,iterator 都是弱一致性的。

hashmap强一致性:HashMap则抛出了ConcurrentModificationException,因为HashMap包含一个修改计数器modCount,当你调用他的next()方法来获取下一个元素时,迭代器将会用到这个计数器。

两者应用具体分析:

选择哪一个,是在性能与数据一致性之间权衡。ConcurrentHashMap适用于追求性能的场景,大多数线程都只做insert/delete操作,对读取数据的一致性要求较低。

5.ConcurrentHashMap如何发生ReHash?

ConcurrentLevel并发级别( 实际上是Segment的实际数量 默认16) 一旦设定的话,就不会改变。ConcurrentHashMap当元素个数大于临界值的时候,就会发生扩容。但是ConcurrentHashMap与其他的HashMap不同的是,它不会对Segment 数量增大,只会增加Segment 后面的链表容量的大小。即对每个Segment 的元素进行的ReHash操作。

巨人的肩膀

https://juejin.cn/post/6844903937867268109

上一篇 下一篇

猜你喜欢

热点阅读