CopyOnWriteArrayList 的set为什么要复制?扩容为什么一个一个来,而不是1.5倍

扩容可以理解,set为什么要复制?

参考:

CopyOnWriteArray的使用



ArrayList 的一个线程安全的变体,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。 

这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。 


针对iterator使用了一个叫 COWIterator的阉割版迭代器,因为不支持写操作,当获取CopyOnWriteArrayList的迭代器时,是将迭代器里的数据引用指向当前 引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全


这个思想在我此前这篇文章   

hashmap与hashtable区别

 里也有涉及,HashMap的迭代器(Iterator)是fail-fast迭代器,所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException

————————————————————————————————————

另一篇文章:http://blog.csdn.net/hzzhoushaoyu/article/details/26146313

然后是这里为什么要将引用指向clone的一个新对象

对于在set之前得到指向老对象引用的不进行干扰,包括getArray和iterator等,而且CopyOnWriteArrayList在大多为读操作时才使用,写操作性能较差。发布一个不可变对象,是不需要进一步的同步操作这是CopyOnWrite的核心思路。


作者不仅提到了迭代器的安全,还提到了不可变对象在读操作时的“0同步处理需求”

同样的,还有String



************************************

(二)再回来看第2个问题:扩容为什么不直接1.5倍的扩,像ArrayList一样

先来看ArrayList为什么要1.5倍的扩,因为下次省的再扩,而copyonwritearraylist不一样,为了使迭代器达到最安全的状态,每次都复制,直接扩大哦1.5倍没有意义


参考原文:

由于之前在弄 eventbus的源码分析时,源码中有用到CopyOnWriteArray 由于时间问题,知道现在才做了一个相应的整理:

---------------------------------------------------------做一枚健康的小码畜--------------------------------------------------

CopyOnWriteArrayList是ArrayList 的一个线程安全的变体(ArrayList不是线程安全的哦),其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。

在源码中可以看到:

复制代码
1 public CopyOnWriteArrayList(Collection<? extends E> c) {
2         Object[] elements = c.toArray();
3         // c.toArray might (incorrectly) not return Object[] (see 6260652)
4         if (elements.getClass() != Object[].class)
//这里是copyof,
5 elements = Arrays.copyOf(elements, elements.length, Object[].class); 6 setArray(elements); 7 }
复制代码

CopyOnWriteArrayList 其实是做了一份拷贝,所以一般需要很大的开销,在android中不建议将context或者activity放到CopyOnWriteArrayList中

 尽管需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set和add)不受支持。这些方法将抛出UnsupportedOperationException。允许使用所有元素,包括null。

在CopyOnWriteArrayList里处理写操作(包括add、remove、set等)是先将原始的数据通过JDK1.6的Arrays.copyof()来生成一份新的数组

然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上(因为要保证写的一致性,这里要对各种写操作要加一把锁,JDK1.6在这里用了重入锁),

然后读的时候就是在引用的当前对象上进行读(包括get,iterator等),不存在加锁和阻塞,针对iterator使用了一个叫 COWIterator的阉割版迭代器,因为不支持写操作,当获取CopyOnWriteArrayList的迭代器时,是将迭代器里的数据引用指向当前 引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全。

CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之 间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合 在多线程里使用,绝对不会发生ConcurrentModificationException ,所以最后得出结论:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。

    内存一致性效果:当存在其他并发 collection 时,将对象放入CopyOnWriteArrayList之前的线程中的操作 happen-before 随后通过另一线程从CopyOnWriteArrayList中访问或移除该元素的操作。 

   这种情况一般在多线程操作时,一个线程对list进行修改。一个线程对list进行fore时会出现java.util.ConcurrentModificationException错误。

   比如:两个线程一个线程fore一个线程修改list的值。


posted on 2018-02-28 23:34  silyvin  阅读(500)  评论(0编辑  收藏  举报