【JUC源码】关于CopyOnWriteArrayList的几个问题

在文章开始之前先放个传送门:【JUC源码】CopyOnWriteArrayList源码分析。对于下面问题有什么不清楚的地方,可以对照着源码进行理解。

1.CopyOnWriteArrayList 和 ArrayList 相比有哪些相同点和不同点?

  • 相同点:底层的数据结构是相同的,都是数组的数据结构,提供出来的 API 都是对数组结构进行操作,让我们更好的使用。

  • 不同点:CopyOnWriteArrayList 是线程安全的,在多线程环境下使用,无需加锁,可直接使用。

2.CopyOnWriteArrayList 通过哪些手段实现了线程安全?

答:主要有:

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

通过以上三点保证了线程安全。

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

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

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

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

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

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

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

6.插入的数据正好在 List 的中间,两种 List 分别拷贝数组几次?为什么?

  • ArrayList 只需拷贝一次,假设插入的位置是 2,只需要把位置 2 (包含 2)后面的数据都往后移动一位即可,所以拷贝一次。

  • CopyOnWriteArrayList 拷贝两次,因为 CopyOnWriteArrayList 多了把老数组的数据拷贝到新数组上这一步。

    扫描二维码关注公众号,回复: 11842057 查看本文章

    可能有人会想到这种方式:先把老数组拷贝到新数组,再把 2 后面的数据往后移动一位,这的确是一种拷贝的方式,但它拷贝的数据总量是 n + n -2 = 2n -2 ,而 CopyOnWriteArrayList 底层实现更加灵活。

    简言之就是,把老数组 0 到 2 的数据拷贝到新数组上,预留出新数组 2 的位置,再把老数组 3~ 最后的数据拷贝到新数组上,这种拷贝方式可以减少我们拷贝的数据,这种方式拷贝的数据总量是n。

    虽然是两次拷贝,但拷贝的数据却仍然是老数组的大小,设计的非常巧妙。

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108782055