再续ConcurrentModificationException - wtstengshen/blog-page GitHub Wiki
####1.举一个例子
/**
错误代码,举例使用
**/
@Test
public void test19() {
List<String> lists = new ArrayList<String>();
lists.add("1");
lists.add("2");
for(String s : lists){
lists.remove(s); //第10行代码
}
}
如果你把上边的代码运行一下,你会发现,呵呵,没有报错,没有异常,没有上一篇文章所说的ConcurrentModificationException.这是怎么回事,为什么不抛异常?其实循环的结果也是不正确的,如果你打印lists的size,会发现是1,解决问题的最根本,还是要看源码,上述执行结果的源码分析下,看注释的解释吧!
/**
1,cursor是迭代器的游标,标识本次循环到哪个位置了
2,size 就是集合的大小;
原因分析:
1,查看源码,你会发现,Iterator的实现类中的
next方法在完成之后,cursor的游标会+1,但是
在例子一中的第10行代码调用了remove方法,这时
集合size会-1;
2,下次循环,再次调用hasNext的时候发现cursor
等于size了,这个迭代器以为游标已经到最后
了,所以循环结束;
3,这个例子虽然没有报错,但是结果是不正确的。
**/
public boolean hasNext() {
return cursor != size;
}
Iterator接口的实现类为什么会ConcurrentModificationException抛异常,这个RuntimeException在设计的时候不能避免吗? 其实,如果在遍历集合的过程中,不抛异常,并不一定就说明本次循环遍历是正确的,Iterator的接口的现实,是一个快速失败的实现, 有别的地方更改了集合的结构,我在继续遍历,结果也不一定是对的,所以抛出异常,这里几种情况,解决不同情景下的方案:
如果你是在单线程的环境下进行遍历,那么只要保证在遍历的过程中,不去修改集合的结构就可以; 如果想在遍历的过程中删除/添加某一符合条件的元素,可以使用具体Iterator接口的实现类的remove方法,或者ListIterator的接口实现类的方法进行操作,不会出现ConcurrentModificationException的异常。
当你阅读ArrayList的API文档的时候,文档会告诉你,ArrayList不是线程安全的实现,可以使用Collections.synchronizedList的方法
List list = Collections.synchronizedList(new ArrayList(...));
转换为线程安全,是不是使用这种方法就能保证遍历的准确性? 答案是:不一定。如果查看一下synchronizedList的源码实现,你会发现:
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
他的实现类的iterator方法并没有实现加锁(Must be manually synched by user!),所以多线程情况下,如果在循环遍历过程中有一个线程更改了另一个线程正在遍历集合的元素,那么还是会抛异常的!如果你自己看源码注释,API上提供了一个解决办法,加锁!(没有什么好办法)
List list = Collections.synchronizedList(new ArrayList());
...
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
如果这样写代码,在遍历的过程中添加了锁,其他的同步方法的操作都会阻塞,直到遍历完成。这种加锁的方式,保证了在遍历过程中的强一致性,我在遍历的过程中,其他线程都不要更改,等我遍历完了,你们在搞。
如果在多线程中一个线程循环遍历,另一个线程进行新增操作,这两个不相互影响,但是前提条件是,循环遍历的线程遍历的数据有可能不是最新的了(如果非要保证循环遍历是强一致性的,只能加锁); 这个时候可以采用CopyOnWriteArrayList这个类来完成,在多线程环境中,该循环循环,改添加添加,不会因为循环遍历加锁导致修改请求阻塞。 CopyOnWriteArrayList的实现原理是:在修改/添加/删除等修改结构操作的时候,修改的是一个copy,然后把修改完成的copy赋值到以前的引用上,就是写时拷贝;关键代码片段:
private volatile transient Object[] array;
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();
}
}
简述一下源码的实现: 关键点: > 1,Object[] array的volatile修饰符; > 2,Arrays.copyOf(elements, len + 1);
首先,添加的方法代码很简单,做了一个数组的copy,然后调用了setArray方法,把新copy的数组,赋值给volatile Object[] array; 1,ReentrantLock实现加锁,线程安全; 2,volatile实现了对所有线程的可见性; 3,每当有修改的时候,都会copy一个新的数组,修改完成之后把array的引用进行修改; 4,CopyOnWriteArrayList类的iterator实现类,并没有显示加锁,因为没有必要加锁,调用iterator方法的实现的时候,会直接循环遍历当前的元素,因为别的线程的修改是copy一个数组进行的修改,所以不会影响当前的遍历; 5,在这种条件下,iterator的循环遍历,有可能不是强一致性,在遍历的时候另一个线程进行了修改,但是当前遍历的还是以前旧引用; 6,CopyOnWriteArrayList的方法用到了Arrays.copyOf方法,会进行数组的copy,如果一个很大的Object[] array进行修改在内存里进行copy,会出现占用内存较多的情况;
以上就先这些,后续进行补充,欢迎进行拍砖。
作者 @悠悠小竹子
博客 iocoding