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的时候,必定要考虑业务上是否可以接受过时读的问题。