本篇内容介绍了“高级并发编程系列之什么是CopyOnWriteArrayList”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
在你看具体内容前,让我们一起先思考这么几个问题:
CopyOnWriteArrayList类名称中,有我们熟悉的ArrayList,那么日常开发使用ArrayList的时候,有什么你需要注意的地方吗?
CopyOnWrite中文翻译过来,是写时复制,到底什么是写时复制呢?
关于写时复制的思想,在什么场景下适合应用,有什么需要注意的地方吗?
带着以上几个问题,让我们一起开始今天的内容吧。
CopyOnWriteArrayList类名称中,包含有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,并通过迭代器进行遍历输出。这样一来我们就准备好了案例基础案例代码。
接下来我们做一些演化操作:
在遍历的过程中,通过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)
上面我们通过案例演示了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。
这也是我重点想要分享给你的地方:持续学习,做到知其然,且知其所以然,一种专研的精神。年轻人少刷点抖音、快手、少看点直播,这些东西除了消耗掉你的精气神外,不会给你带来任何正向价值的东西。
为了方便你理解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不是一个套路对吧。它到底是如何实现的呢?
刚才我们通过CopyOnWriteArrayList,与ArrayList做了案例演示的对比,发现它们在执行结果上有很大的不一样。结果差异的本质原因是CopyOnWriteArrayList类名称中的关键字:CopyOnWrite,中文翻译过来是:写时复制。
到底什么是写时复制呢?所谓写时复制,它直观的含义是:
我已经有了一个集合A,当需要往集合A中添加一个元素,或者删除一个元素的时候
保持A集合不变,从A集合复制一个新的集合B
对应向新集合B中添加、或者删除元素,操作完毕后,将A指向新的B集合,即用新的集合,替换旧的集合
你看这就是写时复制的思想,理解起来并不困难。这样做有什么好处呢?好处就是当我们通过迭代器访问集合的时候,我们可以同时允许向集合中添加、删除集合元素,有效避免了访问集合(读操作),与更新集合(写操作)的冲突,最大化实现了集合的并发访问性能。
那么关于CopyOnWriteArrayList,它是如何最大化提升并发访问能力呢?它的实现原理并不复杂,既然是并发访问,线程安全的问题不可回避,你应该也想到了,首先加锁是必须的。
除了加锁,还需要考虑提升并发访问的能力,如何提升?实现也很简单,针对写操作加锁,读操作不加锁。这样一来,即最大化提升了并发访问的能力,非常适合应用在读多写少的业务场景。这其实也是我们在项目中,使用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”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。