高级并发编程系列十七(一文搞懂CopyOnWriteArrayList)

1.考考你

周末好!今天我要给你分享的是CopyOnWriteArrayList。关于CopyOnWriteArrayList可能你还不太熟悉,由于它在咱们平常开发中,确实用的不算多。它有它的特殊应用场景,也有它比较明显的局限性。java

我之因此专门经过一篇文章给你分享,是想经过CopyOnWriteArrayList把写时复制的思想分享给你。在你看具体内容前,让咱们一块儿先思考这么几个问题:编程

  • CopyOnWriteArrayList类名称中,有咱们熟悉的ArrayList,那么平常开发使用ArrayList的时候,有什么你须要注意的地方吗?设计模式

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

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

带着以上几个问题,让咱们一块儿开始今天的内容吧。数据结构

2.案例

2.1.ArrayList踩过的坑

2.1.1.同祖宗,不相忘

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

2.1.2.ArrayList不能这么用

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

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

咱们知道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,并经过迭代器进行遍历输出。这样一来咱们就准备好了案例基础案例代码。

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

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

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

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添加、删除,都会引发后续的迭代操做抛出异常。你知道这背后的逻辑吗?

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

  • 为何迭代器操做中,不容许向原集合中添加、删除元素?

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

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

 

高清楚为何迭代器操做中,不容许向原集合中添加、删除元素?这个问题后,咱们再进一步看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,中文翻译过来是:写时复制

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

  • 我已经有了一个集合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的时候,必定要考虑业务上是否可以接受过时读的问题。

相关文章
相关标签/搜索