前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java的CopyOnWriteArrayList

Java的CopyOnWriteArrayList

作者头像
用户3467126
发布2019-07-25 15:43:47
5070
发布2019-07-25 15:43:47
举报
文章被收录于专栏:爱编码爱编码

简介

ArrayList并不是线程安全的,在读线程在读取ArrayList的时候如果有写线程在写数据的时候,基于fast-fail机制,会抛出ConcurrentModificationException异常,也就是说ArrayList并不是一个线程安全的容器。那么并发的情况下,这就有了CopyOnWriteArrayList这个东西。

下面主要以下几个方面学习CopyOnWriteArrayList:1)什么是fast-fail机制 2)COW的设计思想 3)add方法实现原理 4)get方法实现原理 5)CopyOnWrite的缺点

原理分析

fast-fail机制

fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

COW的设计思想

COW通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器

对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。

所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性

add方法

通过源码分析可知,CopyOnWriteArrayList使用的数据结构是数组(跟ArrayList一样的)。结构如下

说明:CopyOnWriteArrayList底层使用数组来存放元素。

代码语言:javascript
复制
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;//transient是为了不被序列化的

并且该数组引用是被volatile修饰,注意这里仅仅是修饰的是数组引用

那么现在看看add函数,源码如下:

代码语言:javascript
复制
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //1. 使用Lock,保证写线程在同一时刻只有一个
    lock.lock();
    try {
        //2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;
        //3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //4. 往新数组中添加新的数据
        newElements[len] = e;
        //5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

add方法的逻辑非常好理解:

1、采用ReentrantLock,这样子就把写操作给锁住了,从而就避免的并发。2、数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。3、由于在写数据的时候,是在新的数组中插入数据的,从而保证读写是在两个不同的数据容器中进行操作。

get方法

这里没有任何加锁或CAS操作,因为所有的读线程只是会读取数据容器中的数据,并不会进行修改,并没有出现线程不安全的问题,具体看源码如下:

代码语言:javascript
复制
public E get(int index) {
    return get(getArray(), index);
}
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

CopyOnWrite的缺点

它存在两个问题,即内存占用问题和数据一致性问题

内存占用问题

因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比 如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的minor GC和major GC。

数据一致性问题

CopyOnWrite容器只能保证数据的最终一致性不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

总结

多看书,多看底层,多学设计。

参考文章

https://juejin.im/post/5aeeb55f5188256715478c21 https://www.cnblogs.com/expiator/p/10105419.html http://ifeve.com/java-copy-on-write/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爱编码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 原理分析
    • fast-fail机制
      • COW的设计思想
        • add方法
          • get方法
            • CopyOnWrite的缺点
            • 总结
            • 参考文章
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档