转载地址:http://my.oschina.net/jielucky/blog/167198
一。CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。
这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set和add)不受支持。这些方法将抛出UnsupportedOperationException。允许使用所有元素,包括null。
内存一致性效果:当存在其他并发 collection 时,将对象放入CopyOnWriteArrayList之前的线程中的操作 happen-before 随后通过另一线程从CopyOnWriteArrayList中访问或移除该元素的操作。
这种情况一般在多线程操作时,一个线程对list进行修改。一个线程对list进行fore时会出现java.util.ConcurrentModificationException错误。
下面来看一个列子:两个线程一个线程fore一个线程修改list的值。
package List;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* |CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。
* @author Administrator
*
*/
public class ListThreadTest{
public static class ReadTask implements Runnable{
List<String> list;
public ReadTask(List<String> list) {
this.list = list;
}
public void run() {
for (String str : list) {
System.out.println(str);
}
}
}
private static class WriteTask implements Runnable {
List<String> list;
int index;
public WriteTask(List<String> list, int index) {
this.list = list;
this.index = index;
}
public void run() {
list.remove(index);
list.add(index, "write_" + index);
}
}
public void run() {
final int NUM = 10;
List<String> list = new ArrayList<String>();
for (int i = 0; i < NUM; i++) {
list.add("main_" + i);
}
ExecutorService executorService = Executors.newFixedThreadPool(NUM);
for (int i = 0; i < NUM; i++) {
executorService.execute(new ReadTask(list));
executorService.execute(new WriteTask(list, i));
}
executorService.shutdown();
}
public static void main(String[] args) {
new ListThreadTest().run();
}
}
运行报错:
main_0
main_1
main_2
main_3
main_4
main_5
main_6
main_7
main_8
write_0
write_1
main_2
main_3
main_4
main_5
main_6
main_7
main_8
main_9
write_0
write_1
Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
main_2
write_3
write_0
write_1
main_2
write_3
main_4
main_5
main_6
main_7
main_8
main_9
write_0
write_1
main_2
write_3
main_4
write_5
main_6
main_7
main_8
main_9
write_0
write_1
main_2
write_3
main_4
write_5
write_6
write_0
write_1
main_2
write_3
main_4
write_5
write_6
write_7
main_8
main_9
write_0
main_4
write_0
write_0
write_1
write_2
write_3
write_4
write_5
write_6
write_7
write_8
write_9
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at List.ListThreadTest$ReadTask.run(ListThreadTest.java:23)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Exception in thread "pool-1-thread-3" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at List.ListThreadTest$ReadTask.run(ListThreadTest.java:23)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Exception in thread "pool-1-thread-9" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at List.ListThreadTest$ReadTask.run(ListThreadTest.java:23)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Exception in thread "pool-1-thread-8" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at List.ListThreadTest$ReadTask.run(ListThreadTest.java:23)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
从结果中可以看出来。在多线程情况下报错。其原因就是多线程操作结果:那这个种方案不行我们就换个方案。用jdk自带的类CopyOnWriteArrayList来做容器。这个类和ArrayList最大的区别就是add(E) 的时候。容器会自动copy一份出来然后再尾部add(E)。看源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
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();
}
}
|
用到了Arrays.copyOf 方法。这样导致每次操作的都不是同一个引用。也就不会出现java.util.ConcurrentModificationException错误。
换了种方案看代码:
1
2
|
// List<String> list = new ArrayList<String>();
CopyOnWriteArrayList<String> list =
new
CopyOnWriteArrayList<String>();
|
![](https://i-blog.csdnimg.cn/blog_migrate/f37fea31457071b0c3b44c9690384770.png)
其结果没报错。
CopyOnWriteArrayList add(E) 和remove(int index)都是对新的数组进行修改和新增。所以在多线程操作时不会出现java.util.ConcurrentModificationException错误。
所以最后得出结论:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况。
二:关于java.util.ConcurrentModificationException发生错误的原因,转载自http://fine36.blog.163.com/blog/static/189251005201258113857343/
内容为:
用iterator遍历集合时碰到java.util.ConcurrentModificationException这个异常,
下面以List为例来解释为什么会报java.util.ConcurrentModificationException这个异常,代码如下:
- public static void main(String[] args) {
- List<String> list = new ArrayList<String>();
- list.add("1");
- list.add("2");
- list.add("3");
- list.add("4");
- list.add("5");
- list.add("6");
- list.add("7");
- List<String> del = new ArrayList<String>();
- del.add("5");
- del.add("6");
- del.add("7");
- <span style="color: #ff0000;">for(String str : list){
- if(del.contains(str)) {
- list.remove(str);
- }
- }</span>
- }
运行这段代码会出现如下异常:
for(String str : list) 这句话实际上是用到了集合的iterator() 方法
JDK java.util. AbstractList类中相关源码
java.util. AbstractList的内部类Itr的源码如下:
- private class Itr implements Iterator<E> {
- /**
- * Index of element to be returned by subsequent call to next.
- */
- int cursor = 0;
- /**
- * Index of element returned by most recent call to next or
- * previous. Reset to -1 if this element is deleted by a call
- * to remove.
- */
- int lastRet = -1;
- /**
- * The modCount value that the iterator believes that the backing
- * List should have. If this expectation is violated, the iterator
- * has detected concurrent modification.
- */
- int expectedModCount = modCount;
- public boolean hasNext() {
- return cursor != size();
- }
- public E next() {
- checkForComodification(); //检测modCount和expectedModCount的值!!
- try {
- E next = get(cursor);
- lastRet = cursor++;
- return next;
- } catch (IndexOutOfBoundsException e) {
- checkForComodification();
- throw new NoSuchElementException();
- }
- }
- public void remove() {
- if (lastRet == -1)
- throw new IllegalStateException();
- checkForComodification();
- try {
- AbstractList.this.remove(lastRet); //执行remove的操作
- if (lastRet < cursor)
- cursor--;
- lastRet = -1;
- expectedModCount = modCount; //保证了modCount和expectedModCount的值的一致性,避免抛出ConcurrentModificationException异常
- } catch (IndexOutOfBoundsException e) {
- throw new ConcurrentModificationException();
- }
- }
- final void checkForComodification() {
- if (modCount != expectedModCount) //当modCount和expectedModCount值不相等时,则抛出ConcurrentModificationException异常
- throw new ConcurrentModificationException();
- }
- }
再看一下ArrayList 的 remove方法
- public boolean remove(Object o) {
- if (o == null) {
- for (int index = 0; index < size; index++)
- if (elementData[index] == null) {
- fastRemove(index);
- return true;
- }
- } else {
- for (int index = 0; index < size; index++)
- if (o.equals(elementData[index])) {
- fastRemove(index);
- return true;
- }
- }
- return false;
- }
- /*
- * Private remove method that skips bounds checking and does not
- * return the value removed.
- */
- private void fastRemove(int index) {
- modCount++; //只是修改了modCount,因此modCount将与expectedModCount的值不一致
- int numMoved = size - index - 1;
- if (numMoved > 0)
- System.arraycopy(elementData, index+1, elementData, index,
- numMoved);
- elementData[--size] = null; // Let gc do its work
- }
回过头去看看java.util. AbstractList的next()方法
- public E next() {
- checkForComodification(); //检测modCount和expectedModCount的值!!
- try {
- E next = get(cursor);
- lastRet = cursor++;
- return next;
- } catch (IndexOutOfBoundsException e) {
- checkForComodification();
- throw new NoSuchElementException();
- }
- }
- final void checkForComodification() {
- if (modCount != expectedModCount) //当modCount和expectedModCount值不相等时,则抛出ConcurrentModificationException异常
- throw new ConcurrentModificationException();
- }
- }
现在真相终于大白了,ArrayList的remove方法只是修改了modCount的值,并没有修改expectedModCount,导致modCount和expectedModCount的值的不一致性,当next()时则抛出ConcurrentModificationException异常
因此使用Iterator遍历集合时,不要改动被迭代的对象,可以使用 Iterator 本身的方法 remove() 来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护modCount和expectedModCount值的一致性。
解决办法如下:
(1) 新建一个集合存放要删除的对象,等遍历完后,调用removeAll(Collection<?> c)方法
把上面例子中迭代集合的代码替换成:
- List<String> save = new ArrayList<String>();
- for(String str : list)
- {
- if(del.contains(str))
- {
- save.add(str);
- }
- }
- list.removeAll(save);
(2) 使用Iterator替代增强型for循环:
- Iterator<String> iterator = list.iterator();
- while(iterator.hasNext()) {
- String str = iterator.next();
- if(del.contains(str)) {
- iterator.remove();
- }
- }
Iterator.remove()方法保证了modCount和expectedModCount的值的一致性,避免抛出ConcurrentModificationException异常。
不过对于在多线程环境下对集合类元素进行迭代修改操作,最好把代码放在一个同步代码块内,这样才能保证modCount和expectedModCount的值的一致性,类似如下:
- Iterator<String> iterator = list.iterator();
- synchronized(synObject) {
- while(iterator.hasNext()) {
- String str = iterator.next();
- if(del.contains(str)) {
- iterator.remove();
- }
- }
- }
因为迭代器实现类如:ListItr的next(),previous(),remove(),set(E e),add(E e)这些方法都会调用checkForComodification(),源码:
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
曾经写了下面这段对HashMap进行迭代删除操作的错误的代码:
- Iterator<Integer> iterator = windows.keySet().iterator();
- while(iterator.hasNext()) {
- int type = iterator.next();
- windows.get(type).closeWindow();
- iterator.remove();
- windows.remove(type); //
- }
上面的代码也会导致ConcurrentModificationException的发生。罪魁祸首是windows.remove(type);这一句。
根据上面的分析我们知道iterator.remove();会维护modCount和expectedModCount的值的一致性,而windows.remove(type);这句是不会的。其实这句是多余的,上面的代码去掉这句就行了。
iterator.remove()的源码如下:HashIterator类的remove()方法
- public void remove() {
- if (lastEntryReturned == null)
- throw new IllegalStateException();
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- HashMap.this.remove(lastEntryReturned.key);
- lastEntryReturned = null;
- expectedModCount = modCount; //保证了这两值的一致性
- }
HashMap.this.remove(lastEntryReturned.key);这句代码说明windows.remove(type);是多余的,因为已经删除了该key对应的value。
windows.remove(type)的源码:
- public V remove(Object key) {
- if (key == null) {
- return removeNullKey();
- }
- int hash = secondaryHash(key.hashCode());
- HashMapEntry<K, V>[] tab = table;
- int index = hash & (tab.length - 1);
- for (HashMapEntry<K, V> e = tab[index], prev = null;
- e != null; prev = e, e = e.next) {
- if (e.hash == hash && key.equals(e.key)) {
- if (prev == null) {
- tab[index] = e.next;
- } else {
- prev.next = e.next;
- }
- modCount++;
- size--;
- postRemove(e);
- return e.value;
- }
- }
- return null;
- }
上面的代码中,由于先调用了iterator.remove();所以再调用HashMap的remove方法时,key就已经为null了,所以会执行:removeNullKey();
方法,removeNullKey()源码:
- private V removeNullKey() {
- HashMapEntry<K, V> e = entryForNullKey;
- if (e == null) {
- return null;
- }
- entryForNullKey = null;
- modCount++;
- size--;
- postRemove(e);
- return e.value;
- }
不过不管是执行removeNullKey()还是key != null,如果直接调用HashMap的remove方法,都会导致ConcurrentModificationException
这个异常的发生,因为它对modCount++;没有改变expectedModCount的值,没有维护维护索引的一致性。
下面引用一段更专业的解释:
Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性
三.CopyOnWriteArrayList API方法的源码实现:
http://caoyaojun1988-163-com.iteye.com/blog/1754686
内容为:
一、 核心思想:
CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
二、类图预览:
方法基本分为CopyOnWriteArrayList、indexOf、contains、get、set、add、remove、addIfAbsent和iterator几类:
1、CopyOnWriteArrayList 构造方法:
基本使用Arrays.copyOf 方法,将参数的集合类设置到array属性上。
2、indexOf方法:
简单的通过循环,对比找到所在的位置,核心代码:
- for (int i = index; i < fence; i++)
- if (o.equals(elements[i]))
- return i;
值得注意有两点,一是支持NULL对象、二是lastIndexOf从后面往前,提高性能
3、 contains方法:
该方法使用indexOf方法,避免代码重复。containsAll方法也是简单的循环判断是否包含单个元素。
4、get方法:
直接返回对应下标元素
5、set方法:
- public E set(int index, E element) {
- final ReentrantLock lock = this.lock;
- lock.lock();
- try {
- Object[] elements = getArray();
- E oldValue = get(elements, index);
- if (oldValue != element) {
- int len = elements.length;
- Object[] newElements = Arrays.copyOf(elements, len);
- newElements[index] = element;
- setArray(newElements);
- } else {
- // Not quite a no-op; ensures volatile write semantics
- setArray(elements);
- }
- return oldValue;
- } finally {
- lock.unlock();
- }
- }
可以看到该法使用ReentrantLock锁, Arrays.copyOf创建一个新的数组是核心思想体现,oldValue != element这个判断更是尽可能的提高性能的努力。
而在esle里面,明明没有任何修改,为什么还要条用set方法,并且在addAllAbsent 方法里面有没有使用,以及那句注释(Not quite a no-op; ensures volatile write semantics),有几封邮件讨论这个问题。
大意是说:为了确保 voliatile 的语义,任何一个读操作都应该是写操作的结构,所以尽管写操作没有改变数据,还是调用set方法,当然这仅仅是语义的说明,去掉也是可以的。而对于 addIfAbsent方法为什么没有使用set方法,那是因为该方法本身的语义就是写或者不写,不写故不需要保持语义。
参考如下:
http://cs.oswego.edu/pipermail/concurrency-interest/2010-February/006886.html
http://cs.oswego.edu/pipermail/concurrency-interest/2010-February/006887.html
http://cs.oswego.edu/pipermail/concurrency-interest/2010-February/006888.html
http://en.usenet.digipedia.org/thread/13652/1242/
6、add方法:
- 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();
- }
- }
同样很简单,遵循,使用锁,Arrays.copyOf copy新数组、新增一个元素、set回去步骤。
另外一个重载的指定位置add元素的核心代码如下:
- newElements = new Object[len + 1];
- System.arraycopy(elements, 0, newElements, 0, index);
- System.arraycopy(elements, index, newElements, index + 1, numMoved);
主要使用System.arraycopy方法copy到一个新的数组
7、remove方法:
- 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)
- 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();
- }
- }
同样很简单,使用 System.arraycopy、Arrays.copyOf移动元素
移除指定元素方法的核心代码:通过双重循环,比较移动。
- for (int i = 0; i < newlen; ++i) {
- if (eq(o, elements[i])) {
- // found one; copy remaining and exit
- for (int k = i + 1; k < len; ++k)
- newElements[k-1] = elements[k];
- setArray(newElements);
- return true;
- } else
- newElements[i] = elements[i];
移除指定集合内方法核心代码:
- for (int i = 0; i < len; ++i) {
- Object element = elements[i];
- if (!c.contains(element))
- temp[newlen++] = element;
- }
- if (newlen != len) {
- setArray(Arrays.copyOf(temp, newlen));
- return true;
- }
8、addIfAbsent 方法:
- public boolean addIfAbsent(E e) {
- final ReentrantLock lock = this.lock;
- lock.lock();
- try {
- // Copy while checking if already present.
- // This wins in the most common case where it is not present
- Object[] elements = getArray();
- int len = elements.length;
- Object[] newElements = new Object[len + 1];
- for (int i = 0; i < len; ++i) {
- if (eq(e, elements[i]))
- return false; // exit, throwing away copy
- else
- newElements[i] = elements[i];
- }
- newElements[len] = e;
- setArray(newElements);
- return true;
- } finally {
- lock.unlock();
- }
- }
这里可以看到没有又相同的元素之间return了,没有调用set方法;
9、retainAll 方法:
- Object[] temp = new Object[len];
- for (int i = 0; i < len; ++i) {
- Object element = elements[i];
- if (c.contains(element))
- temp[newlen++] = element;
- }
基本是removeAll的翻版,只是 if (c.contains(element)) 这个是否定罢了。
10、writeObject、readObject方法:
- private void writeObject(java.io.ObjectOutputStream s)
- throws java.io.IOException{
- s.defaultWriteObject();
- Object[] elements = getArray();
- // Write out array length
- s.writeInt(elements.length);
- // Write out all elements in the proper order.
- for (Object element : elements)
- s.writeObject(element);
- }
- private void readObject(java.io.ObjectInputStream s)
- throws java.io.IOException, ClassNotFoundException {
- s.defaultReadObject();
- // bind to new lock
- resetLock();
- // Read in array length and allocate array
- int len = s.readInt();
- Object[] elements = new Object[len];
- // Read in all elements in the proper order.
- for (int i = 0; i < len; i++)
- elements[i] = s.readObject();
- setArray(elements);
- }
虽然CopyOnWriteArrayList 类实现了 序列化接口,但是变量数组确有transient关键字通过实现这两个方法。将快照序列化
11、iterator 方法:
- public void remove() {
- throw new UnsupportedOperationException();
- }
针对iterator使用了一个叫COWIterator的阉割版迭代器,因为不支持写操作 ,如上面add、set、remove都会跑出异常,当获取CopyOnWriteArrayList的迭代器时,是将迭代器里的数据引用指向当前引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全。
综上:
在CopyOnWriteArrayList里处理写操作(包括add、remove、set等)是先将原始的数据通过Arrays.copyof()来生成一份新的数组,然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,并且加锁。
读操作是在引用的当前对象上进行读(包括get,iterator等),不存在加锁和阻塞。
因为每次使用CopyOnWriteArrayList.add都要引起数组拷贝, 所以应该避免在循环中使用CopyOnWriteArrayList.add。可以在初始化完成后设置到CopyOnWriteArrayList中,或者使用CopyOnWriteArrayList.addAll方法
CopyOnWriteArrayList采用“写入时复制”策略,对容器的写操作将导致的容器中基本数组的复制,性能开销较大。所以在有写操作的情况下,CopyOnWriteArrayList性能不佳,而且如果容器容量较大的话容易造成溢出。