源码-集合集锦-CopyOnWriteArrayList从源码到面试

CopyOnWriteArrayList

感谢大佬的图解分析CopyOnWriteArrayList,让渺小的我受益匪浅

前言

  • CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,这个类是为并发而设计的
  • CopyOnWriteArrayList, 顾名思义,CopyOnWriteArrayList是一个写时复制,也就是说,对于CopyOnWriteArrayList,任何可变的操作(add、set、remove等等)都是伴随着写时复制的操作

四个关注点

关注点 结论
CopyOnWriteArrayList是否允许空 允许
CopyOnWriteArrayList是否允许重复 允许
CopyOnWriteArrayList是否有序 有序
CopyOnWriteArrayList是否线程安全 线程安全

CopyOnWriteArrayList 如何实现写时复制

写时复制的体现主要体现在add方法中
字段声明:

  // 声明一个全局锁,transient作用:属性在保存对象时不被存储
  /** The lock protecting all mutators */
  final transient ReentrantLock lock = new ReentrantLock();
  // 使用volatile修饰array数据, 保证了线程之间的可见性,但是不保证原子性,禁止指令重排
  /** The array, accessed only via getArray/setArray. */
  private transient volatile Object[] array;

  // 提供array的get、set方法
  final Object[] getArray() {
      return array;
  }

  final void setArray(Object[] a) {
      array = a;
  }

下面来看看这段代码:

  public static void main(String[] args) {
      List<Integer> list = new CopyOnWriteArrayList<Integer>();
      list.add(1);
      list.add(2);
  }

构造方法:

  public CopyOnWriteArrayList() {
      setArray(new Object[0]);
  }
  final void setArray(Object[] a) {
      array = a;
  }

可以看到构造方法:底层只是创建了一个长度为0的Object数组,然后实例化一个CopyOnWriteArrayList,用图来表示非常简单:

add方法:

  public boolean add(E e) {
      // 1.获取到全局锁
      final ReentrantLock lock = this.lock;
      // 2.加锁
      lock.lock();
      try {
          // 3. 获取到原来的数组
          Object[] elements = getArray();
          // 长度
          int len = elements.length;
          // 4.数组复制,并且扩容一个位置
          Object[] newElements = Arrays.copyOf(elements, len + 1);
          // 5.将添加的元素加到新扩容的位置上
          newElements[len] = e; 
          // 6.把Object array引用指向新数组
          setArray(newElements);
          return true;
      } finally {
          // 7. 解锁
          lock.unlock();
      }
  }

画图表示add过程:

每一步都清楚地表示在图上了,一次add大致经历了几个步骤:
1、加锁

2、拿到原数组,得到新数组的大小(原数组大小+1),实例化出一个新的数组来

3、把原数组的元素复制到新数组中去

4、新数组最后一个位置设置为待添加的元素(因为新数组的大小是按照原数组大小+1来的)

5、把Object array引用指向新数组

6、解锁
整个过程看起来比较像ArrayList的扩容。有了这个基础,我们再来看一下add了一个整数2做了什么,这应该非常简单了,还是画一张图来表示:

添加过程还是上面的步骤

CopyOnWriteArrayList 透露的思想

(1) 读写分离
我们在读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array数组,但是我们在写的时候,操作的却是复制后的一个新Object[] array数组,也就是说,在读和写的时候操作的不是同一个Object[] array数组,这就是读写分离。这种技术数据库用的非常多,在高并发下为了缓解数据库的压力,即使做了缓存也要对数据库做读写分离,读的时候使用读库,写的时候使用写库,然后读库、写库之间进行一定的同步,这样就避免同一个库上读、写的IO操作太多
(2) 最终一致性
假设有四个线程,线程1 读取CopyOnwriteArrayList中的数据,线程2,3,4分别对CopyOnwriteArrayList做出修改。对CopyOnWriteArrayList来说,线程1读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,但是线程1拿到的还是最老的那个Object[] array,新添加进去的数据并没有,所以线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,但是对于之后的线程一定是一致的,它们拿到的Object[] array一定是三个线程都操作完毕之后的Object array[],这就是最终一致。最终一致对于分布式系统也非常重要,它通过容忍一定时间的数据不一致,提升整个分布式系统的可用性与分区容错性。当然,最终一致并不是任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求非常非常高,就必须做成强一致性的。

最后总结一点,随着CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代价将越来越昂贵,因此,CopyOnWriteArrayList适用于读操作远多于修改操作的并发场景中。

面试题

我们为什么使用CopyOnWriteArrayList

在日常开发中,难免会出现多线程场景,只是在单线程的场景中使用ArrayList、Linked等集合就可以解决问题,但是在多线程场景中,当一个线程对集合使用 迭代器遍历,另一个线程在此时又对集合的元素结构进行修改,此时就会比较集合中modCount属性,如果
modCount属性值和预期的不一样,那么就会抛出java.util.ConcurrentModificationException, 相信大家对这个异常并不陌生吧,那么在多线程环境下,为了保证线程安全, 我们有以下几种方案:

  • 使用Collections.SynchronizedList来构造一个线程安全的List
  • 使用线程安全的集合,比如说Verctor等
  • 使用并发包JUC下的集合,比如说CopyOnWriteArrayList
    那么我们为什么选择使用CopyOnWriteArrayList呢?理由就是: 前两种方案其实原理差不多,都是在方法级别上添加了Synchronized关键字做修饰,大家应该都知道,这种方法级别添加Synchronized性能并不高,锁粒度太大,可能只能使用于数据量并不太大的场景,但是CopyOnWriteArrayList采用的是读写分离和最终一致性的思想,CopyOnWriteArrayList将锁的粒度细化了,CopyOnWriteArrayList在写的时候才会上锁,并且是将原数组复制一份再进行操作,在读取操作的时候并没有对数组进行上锁,这就大大提高了并发量

CopyOnWriteArrayList 如何实现线程安全

  1. 定义了全局锁ReentrantLock, 在对CopyOnWriteArrayList 进行操作的时候都会先上锁,比如add、remove、set等方法都又使用到全局锁
  2. 使用volatile修饰了Object[] array,保证了线程之间的及时可见性

CopyOnWriteArrayList为什么并发安全且性能比Vector好?

我们知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获取锁,性能就会大大降低,
而CopyOnWriteArrayList只是在增删改上加ReentrantLock独占锁,但是读操作不加锁,支持并发读,CopyOnWriteArrayList支持读多写少的情况。

以上只是在学习过程中的一点拙见,还请各位多多点评


未完待续, 后面会继续补充相关面试题

posted @   YiGe96_XH  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示