并发 List、Map 面试题

并发 List、Map 面试题

CopyOnWriteArrayList

和 ArrayList 相比有什么相同点和不同点

  • 相同点:

    底层数据结构都是数组

  • 不同点:

    CopyOnWriteArrayList 是线程安全的,在多线程环境下使用,无需加锁,可直接使用

CopyOnWriteArrayList 通过哪些手段保证了线程安全

  • 数组容器用 volatile 关键字修饰,保证了数组内存地址被任意线程修改后,都会通知到其他线程
  • 所有对数组的修改操作,都采用 ReentrantLock 加锁,保证了同一时刻,只能有一个线程对数组进行修改,比如在 add 的时候,另一个线程无法 remove
  • 修改过程中对原数组进行了复制,操作是在新数组上修改的,不会对原数组产生影响

在 add 方法中,对数组进行加锁,不是已经是线程安全了吗?为什么还需要对老数组进行拷贝

的确,对数组进行加锁后,能够保证同一时刻,只有一个线程能对数组进行操作,在单核 CPU 下的多线程环境肯定是没有问题,但在多核 CPU 环境下,如果我们不通过复制拷贝新数组,修改数组容器内存地址的话,无法触发 volatile 可见性效果的,那么其他 CPU 下的线程就无法感知数组原来已经被修改了,就会引发多核 CPU 下的线程安全问题。

假设我们不复制拷贝,而是在原来数组上直接修改值,数组的内存地址就不会变,而数组被 volatile 修饰时,必须当数组的内存地址变更时,才能及时的通知到其他线程,内存地址不变,仅仅是数组元素值发生变化时,是无法把数组元素值发生变动的事实,通知到其它线程的。

对老数组进行拷贝,会有性能损耗,我们平时使用需要注意什么

批量操作时,尽量使用 addAll、removeAll 方法,而不要在循环里使用 add、remove 方法,主要是因为在循环里面使用 add、remove 方法,每次在操作时,都会进行一次数组的拷贝(甚至多次),非常耗性能,而 addAll、removeAll 方法底层做了优化,整个操作只会进行一次数组拷贝。当批量操作的数据越多时,批量方法的高性能体现越明显。

为什么 CopyOnWriteArrayList 迭代过程中,数组结构变动,不会抛出 ConcurrentModificationException

主要是因为 CopyOnWriteArrayList 每次操作时,都会产生新的数组,而迭代时,持有的仍然是老数组的引用,所以我们说的数组结构变动,是用新数组替换了老数组,老数组的结构并没有发生变化,所以不会抛出异常。

插入的数组正好在 List 中间,ArrayList 和 CopyOnWriteArrayList 分别拷贝数组几次

  • ArrayList 只需拷贝一次,假设插入的位置是 2 ,只需把位置 2(包含2) 后面的数据都往后移动一位即可
  • CopyOnWriteArrayList 拷贝两次,因为 CopyOnWriteArrayList 多了把老数组的数据拷贝到新数组上这一步,可能有的同学会想到这种方式:先把老数组拷贝到新数组,再把 2 后面的数据往后移动一位,这的确是一种拷贝的方式,但 CopyOnWriteArrayList 底层实现更加灵活,而是:把老数组 0 到 2 的数据拷贝到新数组上,预留出新数组 2 的位置,再把老数组 3~ 最后的数据拷贝到新数组上,这种拷贝方式可以减少我们拷贝的数据,虽然是两次拷贝,但拷贝的数据却仍然是老数组的大小,设计的非常巧妙。

ConcurrentHashMap

ConcurrentHashMap 和 HashMap 的相同点和不同点

  • 相同点:
    • 都是数组 + 链表 + 红黑树 的数据结构
    • 都实现了 Map 接口,继承了 AbstractMap 抽象类,所以两者的方法大多数是相似的,可以互相切换
  • 不同点:
    • ConcurrentHashMap 是线程安全的,在多线程环境下,无需加锁,可直接使用
    • 数据结构上,ConcurrentHashMap 多了转移节点,主要用于保证扩容时的线程安全

ConcurrentHashMap 通过哪些手段保证了线程安全

  • 储存 Map 数据的数组被 volatile 关键字修饰,使用 Unsafe#getObjectVolatile 方法查看值,保证每次获取到的值都是最新的
  • put 时,如果指定的下标索引没有值的话,采用 自旋 + CAS 来保证一定可以新增成功,又不会覆盖其他线程 put 进去的值
  • 如果 put 的节点正在扩容,会等待扩容完成之后再进行 put,保证了在扩容时,老数组的值不会发生变化
  • 对数组的槽点操作时,会先锁住槽点,保证只有当前线程才能对槽点上的链表或者红黑树进行操作
  • 红黑树旋转时,会锁住根节点,保证旋转时的线程安全

描述下 CAS 算法在 ConcurrentHashMap 中的应用

CAS 其实是一种乐观锁,一般有三个值,分别是 赋值对象,原值,新值。在修改值的时候,会先判断内存中的值是否和原值相等,相等的话把新值赋值给对象,否则赋值失败,整个过程是原子性操作,没有线程安全问题

ConcurrentHashMap 的 put 方法中,有使用到 CAS,是结合死循环一起使用的,步骤如下:

  1. 算出索引下标,拿到对应下标的原值
  2. CAS 覆盖当前下标的值,赋值的过程,如果内存中的值和原值相等,则赋值,退出循环
  3. 进行下次循环,重复执行 1、2,直到成功为止

这样做的好处,不会盲目的覆盖原值,而且一定可以赋值成功

ConcurrentHashMap 是如何发现当前槽点正在扩容的

ConcurrentHashMap 新增了一个节点类型,转移节点,当我们发现当前槽点是转移节点时,(转移节点的hash值是 -1),即表示Map 正在进行扩容

发现槽点正在扩容时,put操作会怎么办

自旋或者走到扩容方法中去,帮助扩容,一直等到扩容完成之后,再执行 put 操作

HashMap 和 ConcurrentHashMap 扩容有什么区别

HashMap 是直接在老数据上进行扩容,多线程环境下,会有线程安全的问题,而 ConcurrentHashMap 就不太一样,扩容过程是这样的:

  1. 从数组的队尾开始拷贝;
  2. 拷贝数组的槽点时,先把原数组槽点锁住,拷贝成功到新数组时,把原数组槽点赋值为转移节点
  3. 从数组的尾部拷贝到头部,没拷贝成功一次,就把原数组的槽点设置成转移节点
  4. 知道所有的数据都拷贝到新数组时,直接把整个数组赋值给数组容器,拷贝完成

简单来说,通过扩容时给槽点加锁,并且把槽点设置为转移节点的策略,保证了ConcurrentHashMap 可以一个一个槽点的转移,保证了扩容时的线程安全。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值