CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的,所以需要很大的开销。
在网上搜索帖子过程中,发现有些帖子上面对CopyOnWriteArrayList线程安全的实验,就是通过定义两个线程,线程1对CopyOnWriteArrayList进行循环读取,线程2对CopyOnWriteArrayList进行插入操作,发现并没有抛出ConcurrentModificationException异常,所以就得出结论说CopyOnWriteArrayList是线程安全的,个人感觉这种方式并不准确。
Fail-Fast机制:
要说清楚这个问题,首先需要了解Java的Fail-Fast机制,Fail-Fast机制也叫做快速失败机制,是指在某个线程在 Collection 上进行迭代时,通常不允许另一个线程修改该 Collection。通常在这些情况下,迭代的结果是不确定的。如果检测到这种行为,一些迭代器实现(包括 JRE 提供的所有通用 collection 实现)可能选择抛出ConcurrentModificationException。执行该操作的迭代器称为快速失败迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险。注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败操作会尽最大努力抛出 ConcurrentModificationException。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是ConcurrentModificationException应该仅用于检测 bug。
下面通过代码看ConcurrentModificationException是如何产生的
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
这是ArrayList中Iterator的实现,从代码中可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount 和expectedModCount是否相等,若不等则抛出ConcurrentModificationException 异常。expectedModCount 是在Itr中定义的,并且不会改变。而modCount是在 AbstractList 中定义的,为全局变量。在调用ArrayList里面的add(),remove()等涉及了改变ArrayList元素的个数的方法时,modCount会加1,这样modCount和expectedModCount就不再相相等,也就会抛出异常,这就是Fail-Fast机制。
所以,ConcurrentModificationException 异常,是因为迭代器在循环过程中做了判断,所以才抛出的。而如果仅仅通过简单的for循环去遍历的话,是不会抛出ConcurrentModificationException的,比如,下面这段代码,即使list在遍历过程中改变,也不会抛出异常。
for (int i = 0; i < 10; i++)
{
list.get(i);
}
但是,foreach遍历方法,本质也是通过Iterator实现的,所以通过foreach遍历,如果list在遍历过程中改变,也会抛出ConcurrentModificationException异常。比如下面的代码,就会抛出异常。
for (Integer i : list)
{
System.out.println(i);
}
所以,网上有些帖子写了当List在遍历的时候,如果被修改了会抛出java.util.ConcurrentModificationException错误。也是不准确的,需要看遍历的方式和迭代器的实现方式。比如CopyOnWriteArrayList的迭代器,就不会抛出
java.util.ConcurrentModificationException。
知道了快速失败机制,现在就可以理解,仅仅通过是否抛出ConcurrentModificationException来判断一个集合是否线程安全是并不准确的。
CopyOnWriteArrayList:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (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();
}
}
上面是CopyOnWriteArrayList的add方法:可以看出,新增时,需要先加锁,然后新建一个长度为原长度加一的数组,把原数组拷进去,最后再把新的数组赋值给原数组。当时看到这里,我就疑惑了,既然最终原数组的引用还是变了,那为什么说是线程安全呢?
接下来看这段代码:
public class ListTest {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<Integer>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Thread1 thread1 = new Thread1(list);
Thread2 thread2 = new Thread2(list);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("遍历list:");
for (Integer i : list) {
System.out.print(i + ",");
}
}
public static class Thread1 extends Thread {
private List<Integer> list;
public Thread1(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
System.out.println("通过foreach对list进行迭代--begin");
for (Integer i : list) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("获取元素:" + i);
System.out.println("list的size:" + list.size() + ",list的hashCode:" + list.hashCode());
}
System.out.println("通过foreach对list进行迭代--end");
}
}
public static class Thread2 extends Thread {
private List<Integer> list;
public Thread2(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
try {
Thread.sleep(50);
for (int i = 10; i < 20; i++) {
list.add(i);
Thread.sleep(100);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
代码的流程是这样的,新建了一个CopyOnWriteArrayList,并往里面放了十个数字,之后新建两个线程,一个线程从list中读元素,另一个线程往list里写元素。
如果把CopyOnWriteArrayList换成ArrayList,那么程序的运行结果是这样的:
通过foreach对list进行迭代--begin
获取元素:0
Exception in thread "Thread-0" java.util.ConcurrentModificationException
list的size:11,list的hashCode:950042116
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at collectiontest.ListTest$Thread1.run(ListTest.java:116)
遍历list:
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
Process finished with exit code 0
分析过程:线程1先拿到list进行迭代,并且取到第一个元素(此时还没有打印到控制台),然后睡眠100ms,线程2先往数组里面放入了10,然后睡眠100ms,线程1把“获取元素:0”打印到控制台,之后又迭代取下一个元素,此时由于线程2已经修改过了这个list,所以抛出异常。最终程序结束之前,打印整个list,为0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19。
当list的实现为CopyOnWriteArrayList时,结果为:
通过foreach对list进行迭代--begin
获取元素:0
list的size:11,list的hashCode:950042116
获取元素:1
list的size:12,list的hashCode:-613465465
获取元素:2
list的size:13,list的hashCode:-1837560219
获取元素:3
list的size:14,list的hashCode:-1129791928
获取元素:4
list的size:15,list的hashCode:-663811386
获取元素:5
list的size:16,list的hashCode:896683529
获取元素:6
list的size:17,list的hashCode:2027385639
获取元素:7
list的size:18,list的hashCode:-1575554614
获取元素:8
list的size:19,list的hashCode:-1597552760
获取元素:9
list的size:20,list的hashCode:2015472011
通过foreach对list进行迭代--end
遍历list:
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
Process finished with exit code 0
从结果来看,并没有抛出ConcurrentModificationException异常,但是结果有点奇怪,foreach一共循环了10次,每次打印的list的size和hashCode都有变化,最后一次循环打印的list大小为20,并且hashCode值都不一样,这说明每次add操作之后,list确实有变化了,而且最终打印的list也是有20个元素,那为什么foreach循环只打印了10次呢?而且迭代过程中list明明变了,为什么不抛出异常呢?
接下来看下CopyOnWriteArrayList的Iterator实现:
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code set}
* is not supported by this iterator.
*/
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code add}
* is not supported by this iterator.
*/
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
看下next()方法,CopyOnWriteArrayList的Iterator在遍历过程中,并没有定义抛出ConcurrentModificationException异常,只是遍历返回元素。所以在CopyOnWriteArrayList遍历的时候,即使list改变了,也不会抛异常。
那为什么线程1在遍历的时候,只打印了10个元素呢,可以看出,迭代器在返回数据的时候,是取的snapshot中的第cursor个元素,而snapshot是final的,这就说明snapshot存放对象的引用是不会变的,同时,CopyOnWriteArrayList每次add操作的时候,总是去新建一个新的数组,然后改变对数组的引用,所以,snapshot在迭代器创建的时候,就固定了。之后在遍历过程中,即使有其它线程对CopyOnWriteArrayList进行写操作,迭代器也是拿到的以前数组的快照,这些写操作对迭代器是不可见的。所以,遍历的时候只打印了10个元素。