CopyOnWriteArrayList实现原理及源码分析

举报
Tracy 发表于 2019/11/04 15:28:54 2019/11/04
【摘要】 CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。本文会对CopyOnWriteArrayList的实现原理及源码进行分析。实现原理我们都知道,集合框架中...

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。本文会对CopyOnWriteArrayList的实现原理及源码进行分析。

实现原理

我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。

很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

优缺点分析

了解了CopyOnWriteArrayList的实现原理,分析它的优缺点及使用场景就很容易了。

优点

读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了

缺点

缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

源码分析

基本原理了解了,CopyOnWriteArrayList的代码实现看起来就很容易理解了。

public boolean add(E e) {        //ReentrantLock加锁,保证线程安全        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();        }    }

添加的逻辑很简单,先将原容器copy一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。

删除操作

public E remove(int index) {        //加锁        final ReentrantLock lock = this.lock;        lock.lock();        try {            Object[] elements = getArray();            int len = elements.length;            E oldValue = get(elements, index);            int numMoved = len - index - 1;            if (numMoved == 0)                //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用                setArray(Arrays.copyOf(elements, len - 1));            else {                //否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用                Object[] newElements = new Object[len - 1];                System.arraycopy(elements, 0, newElements, 0, index);                System.arraycopy(elements, index + 1, newElements, index,                                 numMoved);                setArray(newElements);            }            return oldValue;        } finally {            //解锁            lock.unlock();        }    }

删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。

我们再来看看读操作,CopyOnWriteArrayList的读操作是不用加锁的,性能很高。

public E get(int index) {        return get(getArray(), index);}

直接读取即可,无需加锁

 private E get(Object[] a, int index) {        return (E) a[index];    }

总结

本文对CopyOnWriteArrayList的实现原理和源码进行了分析,并对CopyOnWriteArrayList的优缺点也进行了分析(Java并发包中还提供了CopyOnWriteSet,原理类似)。其实所谓并发容器的优缺点,无非是取决于我们在面对特定并发场景时,是否能做出相对合理的选择和应用。也希望本文能帮助到有需要的童鞋,共勉。

原文链接

https://www.cnblogs.com/chengxiao/p/6881974.html


-更多文章-

2018年文章汇总

Java并发编程73道面试题及答案——稳了

Java中锁的分类

一次生产 CPU 100% 排查优化实践

Spring AOP 增强框架 Nepxion Matrix 详解

现身说法:37岁老码农找工作

线程池的工作原理与源码解读


-关注我-

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

转载声明:本文转载自公众号【方志朋】。

原文链接:

https://mp.weixin.qq.com/s?__biz=MzAxNjk4ODE4OQ==&mid=2247484890&idx=1&sn=3cc8c3f059c7344a4b2a23c650c0428c&chksm=9bed24a8ac9aadbeb47395e8b2fa9e43c44e40e6e4d6c78ac49f8ec04f4cd8363375b77fa15d&scene=21#wechat_redirect

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。