千家信息网

高级并发编程系列之什么是CopyOnWriteArrayList

发表于:2025-01-19 作者:千家信息网编辑
千家信息网最后更新 2025年01月19日,本篇内容介绍了"高级并发编程系列之什么是CopyOnWriteArrayList"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大
千家信息网最后更新 2025年01月19日高级并发编程系列之什么是CopyOnWriteArrayList

本篇内容介绍了"高级并发编程系列之什么是CopyOnWriteArrayList"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

1.考考你

在你看具体内容前,让我们一起先思考这么几个问题:

  • CopyOnWriteArrayList类名称中,有我们熟悉的ArrayList,那么日常开发使用ArrayList的时候,有什么你需要注意的地方吗?

  • CopyOnWrite中文翻译过来,是写时复制,到底什么是写时复制呢?

  • 关于写时复制的思想,在什么场景下适合应用,有什么需要注意的地方吗?

带着以上几个问题,让我们一起开始今天的内容吧。

2.案例

2.1.ArrayList踩过的坑

2.1.1.同祖宗,不相忘

CopyOnWriteArrayList类名称中,包含有ArrayList,这表明它们之间具有血缘关系,起源于一个老祖宗,我们先来看类图:

2.1.2.ArrayList不能这么用

通过类图我们看到CopyOnWriteArrayList、ArrayList都实现了相同的接口。为了方便你更好的理解CopyOnWriteArrayList,我们先从ArrayList讲起。

接下来我将通过日常开发中使用ArrayList,我将给你分享需要有意识避开的一些案例。

我们知道ArrayList底层是基于数组数据结构实现,它的特性是:拥有数组的一切特性,且支持动态扩容。那么我们使用ArrayList,其实是把它作为容器来使用,对于容器,你能想到都有哪些常规操作吗?

  • 将元素放入容器中

  • 更新容器中的某个元素

  • 删除容器中的某个元素

  • 获取容器中的某个元素

  • 循环遍历容器中的元素

以上都是我们在项目中,使用容器时的一些高频操作。对于每个操作,我就不带着你一一演示了,你应该都很熟悉。这里我们重点关注循环遍历容器中的元素这个操作。

我们知道容器的循环遍历操作,可以通过for循环遍历,还可以通过迭代器循环遍历。通过上面的类图,我们知道ArrayList顶层实现了Iterable接口,所以它是支持迭代器操作的,这里迭代器,即应用了迭代器设计模式。关于设计模式的内容,我们暂且不去深究,时间允许的话,我将在下一个系列与你分享我理解的面向对象编程、设计原则、设计思想与设计模式。

接下来我通过ArrayList迭代器遍历过程中,需要留意的一些地方。我们直接上代码(show me the code):

package com.anan.edu.common.newthread.collection;import java.util.ArrayList;import java.util.Iterator;/** * 演示ArrayList迭代器遍历时,需要注意的细节 * * @author ThinkPad * @version 1.0 * @date 2020/12/26 10:50 */public class ShowMeArrayList {    public static void main(String[] args) {        // 创建一个ArrayList        ArrayList list = new ArrayList<>();                // 添加元素        list.add("zhangsan");        list.add("lisi");        list.add("wangwu");        /*        * 正常循环迭代输出        * */        Iterator iter = list.iterator();        while(iter.hasNext()){            System.out.println("当前从容器中获取的人是:"+ iter.next());        }    }}

执行结果:

当前从容器中获取的人是:zhangsan当前从容器中获取的人是:lisi当前从容器中获取的人是:wangwu

通过创建ArrayList实例,添加三个元素:zhangsan 、lisi、wangwu,并通过迭代器进行遍历输出。这样一来我们就准备好了案例基础案例代码。

接下来我们做一些演化操作:

  • 在遍历的过程中,通过ArrayList添加、或者删除集合中的元素

  • 在遍历的过程中,通过迭代器Iterator删除集合中的元素

show me code:

/** 遍历过程中,通过Iterator实例:删除元素* 预期结果:正常执行* */Iterator iter = list.iterator();while(iter.hasNext()){   // 如果当前遍历到lisi,我们将lisi从集合中删除   String name = iter.next();   if("lisi".equals(name)){        iter.remove();// 不会抛出异常   why?    }       System.out.println("当前从容器中获取的人是:"+ name);}System.out.println("删除元素后,集合中还有元素:" + list);  // 执行结果当前从容器中获取的人是:zhangsan当前从容器中获取的人是:lisi当前从容器中获取的人是:wangwu删除元素后,集合中还有元素:[zhangsan, wangwu] /******************************************************/    /** 遍历过程中,通过ArrayList实例:添加、或者删除元素* 预期结果:遍历抛出异常* */Iterator iter = list.iterator();while(iter.hasNext()){    // 如果当前遍历到lisi,我们向集合中添加:小明    String name = iter.next();    if("lisi".equals(name)){       list.add("小明");// 这行代码后,继续迭代器抛出异常  why?    }     System.out.println("当前从容器中获取的人是:"+ name);}// 执行结果当前从容器中获取的人是:zhangsanException in thread "main" java.util.ConcurrentModificationException当前从容器中获取的人是:lisi        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)        at java.util.ArrayList$Itr.next(ArrayList.java:859)        at com.anan.edu.common.newthread.collection.ShowMeArrayList.main(ShowMeArrayList.java:31)
2.1.3.背后的逻辑

上面我们通过案例演示了ArrayList在迭代操作的时候,通过迭代器删除元素操作,程序不会抛出异常;通过ArrayList添加、删除,都会引起后续的迭代操作抛出异常。你知道这背后的逻辑吗?

关于这个问题,我从两个角度给你分享:

  • 为什么迭代器操作中,不允许向原集合中添加、删除元素?

  • ArrayList中,是如何控制迭代操作中,如何检测原集合是否被添加、删除操作过?

为了讲清楚这个问题,我们从图开始(一图胜千言):

高清楚为什么迭代器操作中,不允许向原集合中添加、删除元素?这个问题后,我们再进一步看ArrayList是如何检测控制,在迭代过程中,原集合有添加、或者删除操作这个问题。

这里我将带你看一下源代码,这也是我建议你应该要经常做的事情,养成看源代码习惯,我们常说:源码之下无秘密。

/**ArrayList的迭代器,是一个内部类*//*** An optimized version of AbstractList.Itr*/private class Itr implements Iterator {    // 迭代器内部游标,标识下一个待遍历元素的数组下标    int cursor;       // index of next element to return    // 标识已经迭代的最后一个元素的数组下标    int lastRet = -1; // index of last element returned; -1 if no such        // 注意:这个变量很重要,它是整个迭代器迭代过程中    // 标识原集合被添加、删除操作的次数    // 初始值是集合中的成员变量:modCount(集合被添加、删除操作计数值)    int expectedModCount = modCount;   Itr() {}    ........................}/**迭代器 hasNext方法*/public boolean hasNext() {    // 简单判断 cursor是否等于 size    // 相等,则遍历结束    // 不相等,则继续遍历   return cursor != size;}/**迭代器 next方法*/public E next() {  // 关键代码:检查原集合是否被添加、或者删除操作  // 如果有添加,或者删除操作,那么expectedModCount != modCount  // 抛出异常  checkForComodification();  int i = cursor;  if (i >= size)     throw new NoSuchElementException();  Object[] elementData = ArrayList.this.elementData;  if (i >= elementData.length)       throw new ConcurrentModificationException();  cursor = i + 1;   return (E) elementData[lastRet = i];}/**迭代器 checkForComodification方法*/final void checkForComodification() {    if (modCount != expectedModCount)        throw new ConcurrentModificationException();}

通过ArrayList内部类迭代器Itr的源码分析,我们看到迭代器的源码实现非常简答,并且恭喜你!在不知觉中你还学会了迭代器设计模式的实现。

最后我们再通过查看ArrayList中add、remove方法的源码,解惑modCount成员变量的问题:

/**ArrayList 的add方法*//*** Appends the specified element to the end of this list.* @param e element to be appended to this list* @return true (as specified by {@link Collection#add})*/public boolean add(E e) {  // 注释说了:会将modCount成员变量加1   //继续看ensureCapacityInternal方法  ensureCapacityInternal(size + 1);  // Increments modCount!!  elementData[size++] = e;  return true;}/**ArrayList 的ensureCapacityInternal方法*重点是ensureExplicitCapacity方法*/private void ensureExplicitCapacity(int minCapacity) {  // 将modCount变量加1  modCount++;  // overflow-conscious code  if (minCapacity - elementData.length > 0)      // 扩容操作,留给你去看了      grow(minCapacity);}/**ArrayList 的remove方法*//*** Removes the element at the specified position in this list.* Shifts any subsequent elements to the left (subtracts one from their* indices).** @param index the index of the element to be removed* @return the element that was removed from the list* @throws IndexOutOfBoundsException {@inheritDoc}*/public E remove(int index) {  rangeCheck(index);  // 将modCount变量加1  modCount++;  E oldValue = elementData(index);  int numMoved = size - index - 1;  if (numMoved > 0)     System.arraycopy(elementData, index+1, elementData, index,                             numMoved);   elementData[--size] = null; // clear to let GC do its work   return oldValue;}

通过图、和源码分析的方式,现在你应该可以更好的理解ArrayList、和它的内部迭代器Itr,并且在你的项目中可以很好的使用ArrayList。

这也是我重点想要分享给你的地方:持续学习,做到知其然,且知其所以然,一种专研的精神。年轻人少刷点抖音、快手、少看点直播,这些东西除了消耗掉你的精气神外,不会给你带来任何正向价值的东西

2.2.CopyOnWriteArrayList详解

2.2.1.CopyOnWriteArrayList初体验

为了方便你理解CopyOnWriteArrayList,我煞费苦心的带你一路分析ArrayList。现在让我们先直观的看一下CopyOnWriteArrayList。还是通过前面的案例,即迭代器迭代过程中,给原集合添加,或者删除元素。

我们通过ArrayList演示案例的时候,你还记得吧,会抛出异常,至于异常的原因在前面的内容中,我带你一起做了专门的分析。如果你不记得了,建议回头再去看一看

现在我重点通过CopyOnWriteArrayList来演示案例,看在相同的场景下,是否还会抛出异常?你需要重点关心一下这个地方

show me the code:

package com.anan.edu.common.newthread.collection;import java.util.Iterator;import java.util.concurrent.CopyOnWriteArrayList;/** * 演示CopyOnWriteArrayList迭代器遍历时,需要注意的细节 * * @author ThinkPad * @version 1.0 * @date 2020/12/26 10:50 */public class ShowMeCopyOnWriteArrayList {   public static void main(String[] args) {     // 创建一个CopyOnWriteArrayList     CopyOnWriteArrayList list =new CopyOnWriteArrayList<>();     // 添加元素     list.add("zhangsan");     list.add("lisi");     list.add("wangwu");     /*     * 遍历过程中,通过CopyOnWriteArrayList实例:添加、或者删除元素     * 预期结果:正常执行     * */     Iterator iter = list.iterator();     while(iter.hasNext()){        // 如果当前遍历到lisi,我们向集合中添加:小明        String name = iter.next();        if("lisi".equals(name)){           list.add("小明");// 不会抛出异常   why?        }        System.out.println("当前从容器中获取的人是:"+ name);     }     System.out.println("添加元素后,集合中还有元素:" + list);   }}

执行结果:

当前从容器中获取的人是:zhangsan当前从容器中获取的人是:lisi当前从容器中获取的人是:wangwu添加元素后,集合中还有元素:[zhangsan, lisi, wangwu, 小明]

通过执行结果看到,使用CopyOnWriteArrayList,在迭代器迭代过程中,向原集合中添加了一个新的元素:小明。迭代器继续迭代并不会抛出异常,且最后打印结果显示小明确认已经添加到了集合中

对于这个结果,你是不是感到多少有点意外!感觉与ArrayList不是一个套路对吧。它到底是如何实现的呢?

2.2.2.写时复制思想

刚才我们通过CopyOnWriteArrayList,与ArrayList做了案例演示的对比,发现它们在执行结果上有很大的不一样。结果差异的本质原因是CopyOnWriteArrayList类名称中的关键字:CopyOnWrite,中文翻译过来是:写时复制

到底什么是写时复制呢?所谓写时复制,它直观的含义是:

  • 我已经有了一个集合A,当需要往集合A中添加一个元素,或者删除一个元素的时候

  • 保持A集合不变,从A集合复制一个新的集合B

  • 对应向新集合B中添加、或者删除元素,操作完毕后,将A指向新的B集合,即用新的集合,替换旧的集合

你看这就是写时复制的思想,理解起来并不困难。这样做有什么好处呢?好处就是当我们通过迭代器访问集合的时候,我们可以同时允许向集合中添加、删除集合元素,有效避免了访问集合(读操作),与更新集合(写操作)的冲突,最大化实现了集合的并发访问性能

那么关于CopyOnWriteArrayList,它是如何最大化提升并发访问能力呢?它的实现原理并不复杂,既然是并发访问,线程安全的问题不可回避,你应该也想到了,首先加锁是必须的。

除了加锁,还需要考虑提升并发访问的能力,如何提升?实现也很简单,针对写操作加锁读操作不加锁。这样一来,即最大化提升了并发访问的能力,非常适合应用在读多写少的业务场景。这其实也是我们在项目中,使用CopyOnWriteArrayList的一个主要应用场景。

2.2.3.CopyOnWriteArrayList源码分析

通过前面两个小结,我们已经搞清楚CopyOnWriteArrayList的应用场景,并理解了什么是写时复制的思想。在你的项目中,根据业务需要,我们在进行业务结构设计的时候,可以借鉴写时复制的这一思想,解决实际的业务问题。一定要学会活学活用,至于如何发挥,就留给你了。

接下来我带你一起看一下CopyOnWriteArrayList关键方法的源码实现,进一步加深你对写时复制思想的理解,我们通过两个主要的集合操作来看,分别是:

  • 添加集合元素(写操作):add

/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return {@code true} (as specified by {@link Collection#add})*/public boolean add(E e) {  // 写操作,需要加锁  final ReentrantLock lock = this.lock;  lock.lock();  try {     // 复制原集合,且将新元素添加到复制集合中     Object[] elements = getArray();     int len = elements.length;     Object[] newElements = Arrays.copyOf(elements, len + 1);     newElements[len] = e;           // 将新的集合,替换原集合     setArray(newElements);     return true;  } finally {    lock.unlock();  }}
  • 访问集合元素(读操作):get

/*** {@inheritDoc}** @throws IndexOutOfBoundsException {@inheritDoc}*/public E get(int index) {   // 获取集合中的元素,读操作不需要加锁   return get(getArray(), index);}private E get(Object[] a, int index) {        return (E) a[index];}

通过add、get方法源码,验证了我们前面分析的结论:写操作加锁、读操作不需要加锁

最后我们以一个问答的形式结束本次分享,写时复制思想适合应用在读多写少的业务场景下,最大化提升集合的并发访问能力。我们说:任何事物都有两面性,你知道它的另一面存在什么局限性吗?

我们直接给出答案,写时复制思想的局限性是:

  • 更加消耗空间资源,写操作要从旧的集合,复制得到一个新的集合,即新旧集合同时存在,更占用内存资源

  • 另外写操作加锁,读操作不加锁的实现方式,会存在过期读的问题

结合以上两点,当你在项目中应用写时复制思想进行业务架构设计的时候,或者使用CopyOnWriteArrayList的时候,一定要考虑业务上是否能够接受过期读的问题。

"高级并发编程系列之什么是CopyOnWriteArrayList"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

0