copyonwritemaparraylist可以用于什么应用场景

老马说编程
本节以及接下来的几节,我们探讨Java并发包中的容器类。本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理。它们的用法比较简单,我们需要理解的是它们的实现机制,Copy-On-Write,即写时拷贝或写时复制,这是解决并发问题的一种重要思路。
CopyOnWriteArrayList
CopyOnWriteArrayList实现了List接口,它的用法与其他List如ArrayList基本是一样的,它的区别是:
它是线程安全的,可以被多个线程并发访问
它的迭代器不支持修改操作,但也不会抛出ConcurrentModificationException
它以原子方式支持一些复合操作
我们在提到过基于synchronized的同步容器的几个问题。迭代时,需要对整个列表对象加锁,否则会抛出ConcurrentModificationException,CopyOnWriteArrayList没有这个问题,迭代时不需要加锁。在,示例部分代码为:
public static void main(String[] args) {
final List&String& list = Collections
.synchronizedList(new ArrayList&String&());
startIteratorThread(list);
startModifyThread(list);
将list替换为CopyOnWriteArrayList,就不会有异常,如:
public static void main(String[] args) {
final List&String& list = new CopyOnWriteArrayList&&();
startIteratorThread(list);
startModifyThread(list);
不过,需要说明的是,在Java 1.8之前的实现中,CopyOnWriteArrayList的迭代器不支持修改操作,也不支持一些依赖迭代器修改方法的操作,比如Collections的sort方法,看个例子:
public static void sort(){
CopyOnWriteArrayList&String& list = new CopyOnWriteArrayList&&();
list.add("c");
list.add("a");
list.add("b");
Collections.sort(list);
执行这段代码会抛出异常:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.concurrent.CopyOnWriteArrayList$COWIterator.set(CopyOnWriteArrayList.java:1049)
at java.util.Collections.sort(Collections.java:159)
为什么呢?因为Collections.sort方法依赖迭代器的set方法,其代码为:
public static &T extends Comparable&? super T&& void sort(List&T& list) {
Object[] a = list.toArray();
Arrays.sort(a);
ListIterator&T& i = list.listIterator();
for (int j=0; j&a. j++) {
i.set((T)a[j]);
基于synchronized的同步容器的另一个问题是复合操作,比如先检查再更新,也需要调用方加锁,而CopyOnWriteArrayList直接支持两个原子方法:
//不存在才添加,如果添加了,返回true,否则返回false
public boolean addIfAbsent(E e)
//批量添加c中的非重复元素,不存在才添加,返回实际添加的个数
public int addAllAbsent(Collection&? extends E& c)
CopyOnWriteArrayList的内部也是一个数组,但这个数组是以原子方式被整体更新的。每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部的数组引用,这就是写时拷贝。
所有的读操作,都是先拿到当前引用的数组,然后直接访问该数组,在读的过程中,可能内部的数组引用已经被修改了,但不会影响读操作,它依旧访问原数组内容。
换句话说,数组内容是只读的,写操作都是通过新建数组,然后原子性的修改数组引用来实现的。我们通过代码具体来看下。
内部数组声明为:
private volatile transient Object[]
注意,它声明为了volatile,这是必需的,保证内存可见性,写操作更改了之后,读操作能看到。有两个方法用来访问/设置该数组:
final Object[] getArray() {
final void setArray(Object[] a) {
在CopyOnWriteArrayList中,读不需要锁,可以并行,读和写也可以并行,但多个线程不能同时写,每个写操作都需要先获取锁,CopyOnWriteArrayList内部使用ReentrantLock,成员声明为:
transient final ReentrantLock lock = new ReentrantLock();
默认构造方法为:
public CopyOnWriteArrayList() {
setArray(new Object[0]);
就是设置了一个空数组。
add方法的代码为:
public boolean add(E e) {
final ReentrantLock lock = this.
lock.lock();
Object[] elements = getArray();
int len = elements.
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] =
setArray(newElements);
return true;
} finally {
lock.unlock();
代码也容易理解,add方法是修改操作,整个过程需要被锁保护,先拿到当前数组elements,然后复制了个长度加1的新数组newElements,在新数组中添加元素,最后调用setArray原子性的修改内部数组引用。
查找元素indexOf的代码为:
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
也是先拿到当前数组elements,然后调用另一个indexOf进行查找,其代码为:
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = i & i++)
if (elements[i] == null)
for (int i = i & i++)
if (o.equals(elements[i]))
return -1;
这个indexOf方法访问的所有数据都是通过参数传递进来的,数组内容也不会被修改,不存在并发问题。
迭代器方法为:
public Iterator&E& iterator() {
return new COWIterator&E&(getArray(), 0);
COWIterator是内部类,传递给它的是不变的数组,它也只是读该数组,不支持修改。
其他方法的实现思路是类似的,我们就不赘述了。
每次修改都创建一个新数组,然后复制所有内容,这听上去是一个难以令人接受的方案,如果数组比较大,修改操作又比较频繁,可以想象,CopyOnWriteArrayList的性能是很低的。事实确实如此,CopyOnWriteArrayList不适用于数组很大,且修改频繁的场景。它是以优化读操作为目标的,读不需要同步,性能很高,但在优化读的同时就牺牲了写的性能。
之前我们介绍了保证线程安全的两种思路,一种是锁,使用synchronized或ReentrantLock,另外一种是循环CAS,写时拷贝体现了保证线程安全的另一种思路。对于绝大部分访问都是读,且有大量并发线程要求读,只有个别线程进行写,且只是偶尔写的场合,这种写时拷贝就是一种很好的解决方案。
写时拷贝是一种重要的思维,用于各种计算机程序中,比如经常用于操作系统内部的进程管理和内存管理。在进程管理中,子进程经常共享父进程的资源,只有在写时在复制。在内存管理中,当多个程序同时访问同一个文件时,操作系统在内存中可能只会加载一份,只有程序要写时才会拷贝,分配自己的内存,拷贝可能也不会全部拷贝,而只会拷贝写的位置所在的页,页是操作系统管理内存的一个单位,具体大小与系统有关,典型大小为4KB。
CopyOnWriteArraySet
CopyOnWriteArraySet实现了Set接口,不包含重复元素,使用比较简单,我们就不赘述了。内部,它是通过CopyOnWriteArrayList实现的,其成员声明为:
private final CopyOnWriteArrayList&E&
在构造方法中被初始化,如:
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList&E&();
其add方法代码为:
public boolean add(E e) {
return al.addIfAbsent(e);
就是调用了CopyOnWriteArrayList的addIfAbsent方法。
contains方法代码为:
public boolean contains(Object o) {
return al.contains(o);
由于CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的,所以与之前介绍过的Set的实现类如/相比,它的性能比较低,不适用于元素个数特别多的集合。如果元素个数比较多,可以考虑ConcurrentHashMap或ConcurrentSkipListSet,这两个类,我们后续章节介绍。
ConcurrentHashMap与HashMap类似,适用于不要求排序的场景,ConcurrentSkipListSet与TreeSet类似,适用于要求排序的场景。Java并发包中没有与HashSet对应的并发容器,但可以很容易的基于ConcurrentHashMap构建一个,利用Collections.newSetFromMap方法即可。
本节介绍了CopyOnWriteArrayList和CopyOnWriteArraySet,包括其用法和原理,它们适用于读远多于写、集合不太大的场合,它们采用了写时拷贝,这是计算机程序中一种重要的思维和技术。
下一节,我们讨论一种重要的并发容器 - ConcurrentHashMap。
(与其他章节一样,本节所有代码位于 /swiftma/program-logic)
----------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。
阅读排行榜> jdk 五并发包中CopyOnWrite类的用法
jdk 五并发包中CopyOnWrite类的用法
ianzhu & &
发布时间: & &
浏览:2 & &
回复:0 & &
悬赏:0.0希赛币
jdk 5并发包中CopyOnWrite类的用法
  jdk 5的concurrent包中,添加了几个CopyOnWrite开头的类,包括CopyOnWriteHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet。 copy-on-write模式声明了,为了维护对象的一致性快照,要依靠不可变性(immutability)来消除在协调读取不同的但是相关的属性时需要的同步。对于集合,这意味着如果有大量的读(即 get() ) 和迭代,不必同步操作以照顾偶尔的写(即 add() )调用。对于新的 CopyOnWriteArrayList 和 CopyOnWriteArraySet 类,所有可变的(mutable)操作都首先取得后台数组的副本,对副本进行更改,然后替换副本。这种做法保证了在遍历自身更改的集合时,永远不会抛出 ConcurrentModificationException 。遍历集合会用原来的集合完成,而在以后的操作中使用更新后的集合。这些新的集合, CopyOnWriteArrayList 和 CopyOnWriteArraySet ,最适合于读操作通常大大超过写操作的情况。
  如下所示,集合的使用与它们的非 copy-on-write 替代物完全一样。只是创建集合并在其中加入或者删除元素。即使对象加入到了集合中,原来的 Iterator 也可以进行,继续遍历原来集合中的项。
  import java.util.*;
import java.util.concurrent.*;
public class CopyOnWrite {
public static void main(String args[]) {
List list1 = new CopyOnWriteArrayList(Arrays.asList(args));
List list2 = new ArrayList(Arrays.asList(args));
Iterator itor1 = list1.iterator();
Iterator itor2 = list2.iterator();
list1.add("New");
list2.add("New");
printAll(itor1);
} catch (ConcurrentModificationException e) {
System.err.println("Shouldn't get here");
printAll(itor2);
} catch (ConcurrentModificationException e) {
System.err.println("Will get here.");
private static void printAll(Iterator itor) {
while (itor.hasNext()) {
System.out.println(itor.next());
  这个示例程序用命令行参数创建 CopyOnWriteArrayList 和 ArrayList 这两个实例。在得到每一个实例的 Iterator 后,分别在其中加入一个元素。当 ArrayList 迭代因一个 ConcurrentModificationException 问题而立即停止时, CopyOnWriteArrayList 迭代可以继续,不会抛出异常,因为原来的集合是在得到 iterator 之后改变的。如果这种行为(比如通知原来一组事件监听器中的所有元素)是您需要的,那么最好使用 copy-on-write 集合。如果不使用的话,就还用原来的,并保证在出现异常时对它进行处理。
  [1].驯服 Tiger: 并发集合
本问题标题:
本问题地址:
温馨提示:本问题已经关闭,不能解答。
暂无合适的专家
&&&&&&&&&&&&&&&
希赛网 版权所有 & &&java(40)
多线程(7)
除了加锁外,其实还有一种方式可以防止并发修改异常,这就是将读写分离技术(不是数据库上的)。
先回顾一下一个常识:
1、JAVA中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个对象,一个线程获取这个引用指向的对象,那么他们之间不会发生ConcurrentModificationException,他们是在虚拟机层面阻塞的,而且速度非常快,几乎不需要CPU时间。
2、JAVA中两个不同的引用指向同一个对象,当第一个引用指向另外一个对象时,第二个引用还将保持原来的对象。
基于上面这个常识,我们再来探讨下面这个问题:
在CopyOnWriteArrayList里处理写操作(包括add、remove、set等)是先将原始的数据通过JDK1.6的Arrays.copyof()来生成一份新的数组
然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象(这里应用了常识1),这样保证了每次写都是在新的对象上(因为要保证写的一致性,这里要对各种写操作要加一把锁,JDK1.6在这里用了重入锁),
然后读的时候就是在引用的当前对象上进行读(包括get,iterator等),不存在加锁和阻塞,针对iterator使用了一个叫 COWIterator的阉割版迭代器,因为不支持写操作,当获取CopyOnWriteArrayList的迭代器时,是将迭代器里的数据引用指向当前 引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全(这里应用了常识2)。
CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之 间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合 在多线程里使用,绝对不会发生ConcurrentModificationException&,所以最后得出结论:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。
&& 在你的应用中有一个列表(List),它被频繁的遍历,但是很少被修改。像“你的主页上的前十个分类,它被频繁的访问,但是每个小时通过Quartz的Job来调度更新”。
如果你使用ArrayList来作为该列表的数据结构并且不使用同步(synchronization),你可能会遇到ConcurrentModificationException,因为在你使用Quartz的Job修改该列表时,其他的代码可能正在遍历该列表。
&&& 有些开发人员可能使用Vector或Collections.synchronizedList(List&T&)的方式来解决该问题。但是这并没有效果!虽然在列表上add(),remove()和get()方法现在对线程是安全的,但遍历时仍然会抛出ConcurrentModificationException!在你遍历在列表时,你需要在该列表上使用同步,同时,在使用Quartz修改它时,也需要使用同步机制。这对性能和可扩展性来说是一个噩梦。同步需要在所有的地方出现,仅仅是因为每个小时都需要做更新。
&&&& 幸运的是,这里有更好的解决方案。使用CopyOnWriteArrayList。
当列表上的一个结构修改发生时,一个新的拷贝(copy)就会被创建。这在经常发生修改的地方使用,将会很低效。遍历该列表将不会出现ConcurrentModificationException,因为该列表在遍历时将不会被做任何的修改。
另一种避免添加同步代码但可以避免并发修改问题的方式是在调度任务中构建一个新的列表,然后将原来指向到列表上的引用赋值给新的列表。在JVM中,赋值一个新的引用是原子操作。这种方式在使用旧的遍历方式(for (int i=0; i&list.size(); i++) { … list.get(i) …})时将无效(也会出错)。切换的列表中的大小将引发新的错误产生。更加糟糕的是因为改变是在不同的线程中发生的,所以还会有很多潜在的问题。使用volatile关键字可能会有所帮助,但是对列表大小的改变依然会有问题。
&&&& 内存一致性和刚发生后保证了CopyOnWriteArrayList的可用性。同时,代码变得更简单,因为根本不需要使用volatile关键字或同步。更少的代码,更少的bug!
&&&& CopyOnWriteArrayList的另一个使用案例是观察者设计模式。如果事件监听器由多个不同的线程添加和移除,那么使用CopyOnWriteArrayList将会使得正确性和简单性得以保证。
这个类不能用下标去遍历,而要用iterator.
如果用下标遍历的话,与此同时另一个线程去修改了COW,那么可能会报下标out of array.
Java代码&&
public&class&COWT&{&&
&&&&public&static&void&main(String...args)&throws&InterruptedException&&
&&&&&&&&final&CopyOnWriteArrayList&Integer&&cowList&=&new&CopyOnWriteArrayList&Integer&();&&
&&&&&&&&for(int&i=0;i&10;i++)&&
&&&&&&&&{&&
&&&&&&&&&&&&cowList.add(i);&&
&&&&&&&&}&&
&&&&&&&&&&
&&&&&&&&new&Thread(){&&
&&&&&&&&&&&&@Override&&
&&&&&&&&&&&&public&void&run()&{&&
&&&&&&&&&&&&&&&&for(int&i=0;i&cowList.size();i++)&&
&&&&&&&&&&&&&&&&{&&
&&&&&&&&&&&&&&&&&&&&try&{&&
&&&&&&&&&&&&&&&&&&&&&&&&Thread.currentThread().sleep(1);&&
&&&&&&&&&&&&&&&&&&&&}&catch&(InterruptedException&e)&{&&
&&&&&&&&&&&&&&&&&&&&&&&&e.printStackTrace();&&
&&&&&&&&&&&&&&&&&&&&}&&
&&&&&&&&&&&&&&&&&&&&System.out.println(cowList.get(i));&&
&&&&&&&&&&&&&&&&}&&
&&&&&&&&&&&&};&&
&&&&&&&&}.start();&&
&&&&&&&&&&
&&&&&&&&new&Thread(){&&
&&&&&&&&&&&&@Override&&
&&&&&&&&&&&&public&void&run()&{&&
&&&&&&&&&&&&&&&&try&{&&
&&&&&&&&&&&&&&&&&&&&Thread.currentThread().sleep(3);&&
&&&&&&&&&&&&&&&&}&catch&(InterruptedException&e)&{&&
&&&&&&&&&&&&&&&&&&&&e.printStackTrace();&&
&&&&&&&&&&&&&&&&}&&
&&&&&&&&&&&&&&&&cowList.clear();&&
&&&&&&&&&&&&};&&
&&&&&&&&}.start();&&
&&&&&&&&&&
用copyOnWriteList在数据量较大的时候,性能会下降很厉害。
每次的add都会数据拷贝而ArrayList不会,性能较高
我一般的解决方案,还是引用切换的原子操作
另一种避免添加同步代码但可以避免并发修改问题的方式是在调度任务中构建一个新的列表,然后将原来指向到列表上的引用赋值给新的列表。在JVM中,赋值一个新的引用是原子操作。这种方式在使用旧的遍历方式(for (int i=0; i&list.size(); i++) { … list.get(i) …})时将无效(也会出错)。切换的列表中的大小将引发新的错误产生。更加糟糕的是因为改变是在不同的线程中发生的,所以还会有很多潜在的问题。使用volatile关键字可能会有所帮助,但是对列表大小的改变依然会有问题。
List old = new ArrayList();
List temp =
for(int i=0;i&temp.i++){
&&& ...temp.get(i)
或iterator的遍历
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:247384次
积分:3507
积分:3507
排名:第8107名
原创:58篇
转载:317篇
评论:14条
(2)(3)(1)(11)(35)(51)(3)(8)(6)(23)(17)(65)(22)(52)(14)(24)(6)(21)(2)(2)(1)(2)

我要回帖

更多关于 copyonwrite适用场景 的文章

 

随机推荐