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

发布时间:2021-10-21 10:33:00 作者:iii
来源:亿速云 阅读:133

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

1.考考你

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

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

2.案例

2.1.ArrayList踩过的坑

2.1.1.同祖宗,不相忘

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

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

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<String> list = new ArrayList<>();
        
        // 添加元素
        list.add("zhangsan");
        list.add("lisi");
        list.add("wangwu");

        /*
        * 正常循环迭代输出
        * */
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()){
            System.out.println("当前从容器中获取的人是:"+ iter.next());
        }

    }

}

执行结果:

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

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

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

show me code:

/*
* 遍历过程中,通过Iterator实例:删除元素
* 预期结果:正常执行
* */
Iterator<String> 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<String> iter = list.iterator();
while(iter.hasNext()){
    // 如果当前遍历到lisi,我们向集合中添加:小明
    String name = iter.next();
    if("lisi".equals(name)){
       list.add("小明");// 这行代码后,继续迭代器抛出异常  why?
    }
     System.out.println("当前从容器中获取的人是:"+ name);
}

// 执行结果
当前从容器中获取的人是:zhangsan
Exception 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添加、删除,都会引起后续的迭代操作抛出异常。你知道这背后的逻辑吗?

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

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

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

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

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

/*
*ArrayList的迭代器,是一个内部类
*/
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
    // 迭代器内部游标,标识下一个待遍历元素的数组下标
    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 <tt>true</tt> (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<String> list =new CopyOnWriteArrayList<>();

     // 添加元素
     list.add("zhangsan");
     list.add("lisi");
     list.add("wangwu");

     /*
     * 遍历过程中,通过CopyOnWriteArrayList实例:添加、或者删除元素
     * 预期结果:正常执行
     * */
     Iterator<String> 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,中文翻译过来是:写时复制

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

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

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

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

2.2.3.CopyOnWriteArrayList源码分析

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

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

/**
* 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();
  }
}
/**
* {@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”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

推荐阅读:
  1. 并发容器之CopyOnWriteArrayList
  2. 掌握系列之并发编程-9.线程池

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

设计模式

上一篇:git-pull之后怎么查看拉下来的文件有那些修改

下一篇:PHP中如何去完成时区的设置

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》