ArrayList 遍历时删除(添加)常见陷阱及其解决以及fail-fast 机制

list遍历,分为普通遍历,增强for循环,和iterator遍历

1、for循环遍历list

for(int i=0;i<list.size();i++){
    if(list.get(i).equals("del"))
        list.remove(i);
}

删除某个元素后,list的大小发生了变化,而你的索引也在变化,所以会导致你在遍历的时候漏掉某些元素。比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。因此,这种方式可以用在删除特定的一个元素时使用,但不适合循环删除多个元素时使用。

解决方法:

从list最后一元素开始开始遍历。

for(int i=list.size()-1;i>+0;i--){
    if(list.get(i).equals("del"))
        list.remove(i);
}

2、增强for循环

for(String x:list){
    if(x.equals("del"))
        list.remove(x);
}
删除元素后继续循环会报错误信息ConcurrentModificationException,因为元素在使用的时候发生了并发的修改,导致异常抛出

解决:删除完毕马上使用break跳出,则不会触发报错。

for(String x:list){
    if(x.equals("del"))
        list.remove(x);
    break;
}

3、iterator遍历

Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();
    if(x.equals("del")){
        it.remove();
    }
}

这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。

总结:

  (1)循环删除list中特定一个元素的,可以使用三种方式中的任意一种,但在使用中要注意上面分析的各个问题。

  (2)循环删除list中多个元素的,应该使用迭代器iterator方式,或者list从后往前删除。

添加也是上面分析情况。

以下介绍异常产生原因

http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtyfour.html

fail-fast 机制

在 JDK 的 Collection 中我们时常会看到类似于这样的话:

例如,ArrayList:

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。

HashMap 中:

注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

在这两段话中反复地提到”快速失败”。那么何为”快速失败”机制呢?

“快速失败”也就是 fail-fast,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。

一、fail-fast 示例

public class FailFastTest {
	private static List<Integer> list = new ArrayList<>();

	/**
	 * @desc:线程one迭代list
	 * @Project:test
	 * @file:FailFastTest.java
	 */
	private static class threadOne extends Thread{
		public void run() {
			Iterator<Integer> iterator = list.iterator();
			while(iterator.hasNext()){
				int i = iterator.next();
				System.out.println("ThreadOne 遍历:" + i);
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * @desc:当i == 3时,修改list
	 * @Project:test
	 * @file:FailFastTest.java
	 * @Authro:chenssy
	 * @data:2014年7月26日
	 */
	private static class threadTwo extends Thread{
		public void run(){
			int i = 0 ; 
			while(i < 6){
				System.out.println("ThreadTwo run:" + i);
				if(i == 3){
					list.remove(i);
				}
				i++;
			}
		}
	}

	public static void main(String[] args) {
		for(int i = 0 ; i < 10;i++){
			list.add(i);
		}
		new threadOne().start();
		new threadTwo().start();
	}
}

运行结果:


ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
	at java.util.ArrayList$Itr.next(Unknown Source)
	at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)

二、fail-fast 产生原因

通过上面的示例和讲解,我初步知道 fail-fast 产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。

要了解 fail-fast 机制,我们首先要对 ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。

诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出 ConcurrentModificationException 异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。下面我将以 ArrayList 为例进一步分析 fail-fast 产生的原因。

从前面我们知道 fail-fast 是在操作迭代器时产生的。现在我们来看看 ArrayList 中迭代器的源代码:

private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;

public boolean hasNext() {
   return (this.cursor != ArrayList.this.size);
}

public E next() {
	checkForComodification();
	/** 省略此处代码 */
	}

public void remove() {
	if (this.lastRet < 0)
		throw new IllegalStateException();
	checkForComodification();
/** 省略此处代码 */
	}

final void checkForComodification() {
	if (ArrayList.this.modCount == this.expectedModCount)
		return;
	throw new ConcurrentModificationException();
	}
}

从上面的源代码我们可以看出,迭代器在调用 next()、remove() 方法时都是调用 checkForComodification() 方法,该方法主要就是检测 modCount == expectedModCount ? 若不等则抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。所以要弄清楚为什么会产生 fail-fast 机制我们就必须要用弄明白为什么 modCount != expectedModCount ,他们的值在什么时候发生改变的。

expectedModCount 是在 Itr 中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是 modCount。modCount 是在 AbstractList 中定义的,为全局变量:

protected transient int modCount = 0;

那么他什么时候因为什么原因而发生改变呢?请看 ArrayList 的源码:

public boolean add(E paramE) {
	ensureCapacityInternal(this.size + 1);
	/** 省略此处代码 */
}

private void ensureCapacityInternal(int paramInt)   {
	if (this.elementData == EMPTY_ELEMENTDATA)
		paramInt = Math.max(10, paramInt);
	ensureExplicitCapacity(paramInt);
}

private void ensureExplicitCapacity(int paramInt) {
	this.modCount += 1;    //修改modCount
	/** 省略此处代码 */
}

public boolean remove(Object paramObject) {
	int i;
	if (paramObject == null)
		for (i = 0; i < this.size; ++i) {
			if (this.elementData[i] != null)
				continue;
			fastRemove(i);
			return true;
		}
	else
		for (i = 0; i < this.size; ++i) {
			if (!(paramObject.equals(this.elementData[i])))
				continue;
			fastRemove(i);
			return true;
		}
	return false;
} 

private void fastRemove(int paramInt) {
	this.modCount += 1;   //修改modCount
	/** 省略此处代码 */
}

public void clear() {
	this.modCount += 1;    //修改modCount
	/** 省略此处代码 */
}

从上面的源代码我们可以看出,ArrayList 中无论 add、remove、clear 方法只要是涉及了改变 ArrayList 元素的个数的方法都会导致 modCount 的改变。所以我们这里可以初步判断由于 expectedModCount 得值与 modCount 的改变不同步,导致两者之间不等从而产生 fail-fast 机制。知道产生 fail-fast 产生的根本原因了,我们可以有如下场景:

有两个线程(线程 A,线程 B),其中线程 A 负责遍历 list、线程B修改 list。线程 A 在遍历 list 过程的某个时候(此时 expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是 modCount 的值发生改变(modCount + 1 = N + 1)。线程 A 继续遍历执行 next 方法时,通告 checkForComodification 方法发现 expectedModCount = N ,而 modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生 fail-fast 机制。

所以,直到这里我们已经完全了解了 fail-fast 产生的根本原因了。知道了原因就好找解决办法了。

三、fail-fast 解决办法

通过前面的实例、源码分析,我想各位已经基本了解了 fail-fast 的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:

方案一:在遍历过程中所有涉及到改变 modCount 值得地方全部加上 synchronized 或者直接使用 Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

方案二:使用 CopyOnWriteArrayList 来替换 ArrayList。推荐使用该方案。

CopyOnWriteArrayList 为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用 CopyOnWriteArrayList 来替代 ArrayList 再适合不过了。那么为什么 CopyOnWriterArrayList 可以替代 ArrayList 呢?

第一、CopyOnWriterArrayList 的无论是从数据结构、定义都和 ArrayList 一样。它和 ArrayList 一样,同样是实现 List 接口,底层使用数组实现。在方法上也包含 add、remove、clear、iterator 等方法。

第二、CopyOnWriterArrayList 根本就不会产生 ConcurrentModificationException 异常,也就是它使用迭代器完全不会产生 fail-fast 机制。请看:

private static class COWIterator<E> implements ListIterator<E> {
	/** 省略此处代码 */
	public E next() {
		if (!(hasNext()))
			throw new NoSuchElementException();
		return this.snapshot[(this.cursor++)];
	}

	/** 省略此处代码 */
}

CopyOnWriterArrayList 的方法根本就没有像 ArrayList 中使用 checkForComodification 方法来判断 expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以 add 方法为例:

public boolean add(E paramE) {
	ReentrantLock localReentrantLock = this.lock;
	localReentrantLock.lock();
	try {
		Object[] arrayOfObject1 = getArray();
		int i = arrayOfObject1.length;
		Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
		arrayOfObject2[i] = paramE;
		setArray(arrayOfObject2);
		int j = 1;
		return j;
	} finally {
		localReentrantLock.unlock();
	}
}

final void setArray(Object[] paramArrayOfObject) {
	this.array = paramArrayOfObject;
}

CopyOnWriterArrayList 的 add 方法与 ArrayList 的 add 方法有一个最大的不同点就在于,下面三句代码:

Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);

就是这三句代码使得 CopyOnWriterArrayList 不会抛 ConcurrentModificationException 异常。他们所展现的魅力就在于 copy 原来的 array,再在 copy 数组上进行 add 操作,这样做就完全不会影响 COWIterator 中的 array 了。

所以 CopyOnWriterArrayList 所代表的核心概念就是:任何对 array 在结构上有所改变的操作(add、remove、clear 等),CopyOnWriterArrayList 都会 copy 现有的数据,再在 copy 的数据上修改,这样就不会影响 COWIterator 中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的 copy 也是相当有损耗的。


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值