CopyOnWriteArrayList主要可以解决的问题是并发遍历读取无锁(通过Iterator)

对比CopyOnWriteArrayList和ArrayList

假如我们频繁的读取一个可能会变化的清单(数组),你会怎么做?

一个全局的ArrayList(数组),修改时加锁,读取时加锁

读取时为什么需要加锁呢?

如果是ArrayList遍历读取时不加锁,这时其他线程修改了ArrayList(增加或删除),会抛出ConcurrentModificationException,这就是failfast机制(我们这里只讨论Iterator遍历,如果是普通for循环可能会数组越界,这里不讨论)

如果是数组遍历读取时,可能会出现数组越界

所以读锁的是写的操作

如果读加上锁,那么对于并发读来说无疑性能是很糟糕的,当然如果你说用读写锁可以解决这个问题,但是我们这里更期待的是一个无锁的读操作并且能保证线程安全。

下面这个例子营造的背景是相对高并发的读取+相对低并发的修改

List<Integer> arr = new CopyOnWriteArrayList<>();
//List<Integer> arr = new ArrayList<>();//如果通过ArrayList是会报错的
for (int i = 0; i < 3; i++) {
arr.add(i);
}
//多线程读
for (int i = 0; i < 1000; i++) {
final int m = i;
new Thread(() -> {
try {Thread.sleep(1);} catch (InterruptedException e) {}//等等下面写线程的开始
Iterator<Integer> iterator = arr.iterator();
try {Thread.sleep(new Random().nextInt(10));} catch (InterruptedExcep{}//造成不一致的可能性
int count = 0;
while(iterator.hasNext()){
iterator.next();
count++;
}
System.out.println("read:"+count);
}).start();
}
//多线程写
for (int ii = 0; ii < 10; ii++) {
new Thread(() -> {
arr.add(123);
System.out.println("write");
}).start();
}

上面的例子如果更换成ArrayList会报错,原因是:

因为next()方法会调用checkForComodification校验,发现modCount(原始arrayList)与expectedModCount不一致了,这就是上面提到的快速失败,这个快速失败的意思是无论当前是否有并发的情况或问题,只要发现了不一致就抛异常

对于ArrayList解决方案就是遍历iterator时加锁

final void checkForComodification() {
  if (modCount != expectedModCount)
  throw new ConcurrentModificationException();
}

那么为什么换成CopyOnWriteArrayList就可以了呢?我们先不看CopyOnWrite,我们先来分析一下CopyOnWriteArrayList的iterator

public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}

CopyOnWriteArrayList 调用iterator时生成的是一个新的数组快照,遍历时读取的是快照,所以永远不会报错(即使读取后修改了列表),并且在CopyOnWriteArrayList是没有fastfail机制的,原因就在于Iterator的快照实现以及CopyOnWrite已经不需要通过fastfail来保证集合的正确性

CopyOnWriteArrayList的CopyOnWrite即修改数组集合时,会重新创建一个数组并对新数据进行调整,调整完成后将新的数组赋值给老的数组

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();
}
}

为什么要拷贝新的数组,这样做有什么好处?

如果不拷贝新的数组(加锁仍保证其线程安全)直接修改原来的数据结构,那么在读的时候就要加锁了,如果读不加锁就有可能读到修改数组的“半成品”(有可能COWIterator<E>(getArray(), 0);就是个半成品)

而拷贝了新的数组,即使修改没有完成,遍历是拿到的也是老的数组,所以不会有问题。

Doug Lea大神在开发这个类的时候也介绍了这个类的主要应用场景是避免对集合的iterator方法加锁遍历,我们来看一下这个类的注释的节选:

* making a fresh copy of the underlying array.This is ordinarily too costly, but may be more efficient
* than alternatives when traversal operations vastly outnumber
* mutations, and is useful when you cannot or don't want to
* synchronize traversals, yet need to preclude interference among
* concurrent threads.
* This array never changes during the lifetime of the
* iterator, so interference is impossible and the iterator is
* guaranteed not to throw {@code ConcurrentModificationException}.
* The iterator will not reflect additions, removals, or changes to
* the list since the iterator was created.

大概翻译一下:

拷贝一个新的数组这看上去太昂贵了,但是遍历数远远超过变更数时却十分有效,并且在你不想使用synchronized遍历时会更有用

这份新拷贝的数组在iterator生命周期永远不会改变,并且在迭代是不会让生ConcurrentModificationException异常

一旦迭代器创建,则迭代器不能够被修改(添加、删除元素)

我们提取一下作者的思想:

1、这个类使用是线程安全的

2、并发通过迭代器遍历不会报错并且无锁

3、在写少读多的前提下,比较合适

CopyOnWriteArrayList分析——能解决什么问题的更多相关文章

  1. Mybatis关联查询和数据库不一致问题分析与解决

    Mybatis关联查询和数据库不一致问题分析与解决 本文的前提是,确定sql语句没有问题,确定在数据库中使用sql和项目中结果不一致. 在使用SpringMVC+Mybatis做多表关联时候,发现也不 ...

  2. C#中异常:“The type initializer to throw an exception(类型初始值设定项引发异常)”的简单分析与解决方法

    对于C#中异常:“The type initializer to throw an exception(类型初始值设定项引发异常)”的简单分析,目前本人分析两种情况,如下: 情况一: 借鉴麒麟.NET ...

  3. 启动Tomcat一闪而过——分析及解决过程

    启动Tomcat一闪而过--分析及解决过程 嗯,昨天将有关JDK的知识稍微整理了一下,现在稍微整理一下有关Tomcat的! 1:Tomcat是什么? Tomcat是当今世界上使用最为广泛的.开源免费的 ...

  4. SQL Server2005索引碎片分析和解决方法

    SQL Server2005索引碎片分析和解决方法 本文作者(郑贤娴),请您在阅读本文时尊重作者版权. 摘要: SQL Server,为了反应数据的更新,需要维护表上的索引,因而这些索引会形成碎片.根 ...

  5. JavaScript中的ParseInt("08")和“09”返回0的原因分析及解决办法

    今天在程序中出现一个bugger ,调试了好久,最后才发现,原来是这个问题. 做了一个实验: alert(parseInt("01")),当这个里面的值为01====>07时 ...

  6. mybatis异常:Improper inline parameter map format. Should be: #{propName,attr1=val1,attr2=val2}问题分析及解决

    转载自:http://blog.csdn.net/jackpk/article/details/44158701 mybatis异常:Improper inline parameter map for ...

  7. php中session_start()相关问题分析与解决办法

    介绍下,在php中使用session时遇到的一些问题,与相关解决方法.1.错误提示Warning: Cannot send session cookie - headers already sentW ...

  8. 左右c++与java中国的垃圾问题的分析与解决

    左右c++与java中国的垃圾问题的分析与解决 DionysosLai(906391500@qq.com)  2014/8/1 问题分析: 之所以会出现中文乱码问题,归根结底在于中文的编码与英文的编码 ...

  9. 文《左右c++与java中国的垃圾问题的分析与解决》一bug分析

    文<左右c++与java中国的垃圾问题的分析与解决>一bug分析 DionysosLai(906391500@qq.com) 2014/10/21 在前几篇一博客<关于c++与jav ...

随机推荐

  1. ansible编译安装--操作系统环境Redhat6.4

    安装前安装包准备,下载链接如下: Python2.7下载地址:https://www.python.org/ftp/python/2.7.12/Python-2.7.12.tgz ansible下载地 ...

  2. 开源库SRT编译指南

    SRT(Secure,Reliable,Transport)是Haivision公司开发的一套开源媒体传输协议,用于在不稳定的网络环境下,优化媒体数据的传输性能.  SRT的码流加密基于开源库open ...

  3. Codeforces Round #501 (Div. 3) 1015D Walking Between Houses

    D. Walking Between Houses time limit per test 2 seconds memory limit per test 256 megabytes input st ...

  4. ch8 -- directMethod

    稀疏直接法 主要用的g2o的方法.自己定义了一个新的一元边.边的误差项是测量值和由估计得来的x,y对应的灰度值之间的误差.导数为灰度对像素坐标的导数乘以像素坐标对yi*李代数的导数的负数.灰度对于像素 ...

  5. PDO中构建事务处理的应用程序

    <meta http-equiv="Content-Type" content="text/html";charse="utf-8" ...

  6. 跨域和jsonp的了解和学习

    一.为什么会有跨域问题呢 因为有浏览器的同源策略. 同源:如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源.我们也可以把它称为“协议/主机/端口 tuple”,或简单地叫做“ ...

  7. C++之Vect

    在C++中数组和向量都是多同类元素的集合,他们也有很明显的区别 1 数组属于静态分配,编译之前必须知道数组的大小,一旦确定就不能更改:2个数组之间不能直接赋值实现拷贝,而必须显式用for或者拷贝函数拷 ...

  8. 新手写AIDL构建失败:...aidl.exe'' finished with non-zero exit value 1

    最近学习aidl,写demo后编译报错,跟着<Android开发艺术探索>以及网上的一些aidl详解博客敲完后一直编译不过,错误日志如下: Process 'command 'C:\Use ...

  9. properties文件 , properties类, 的作用

    "properties文件",是java所支持的配置文件类型.java中的properties文件是一种配置文件,主要用于表达配置信息,文件类型为*.properties,格式为文 ...

  10. Oracle单行函数。。。

    单行函数 --字符函数--1.ASCII 返回与指定的字符对应的十进制数;select ascii('A') A,ascii('a') a,ascii('0') zero,ascii(' ') spa ...