集合的遍历(多线程)及ConcurrentModificationException

为了保证线程安全,在迭代器迭代的过程中,线程是不能对集合本身进行操作(修改,删除,增加)的,否则会抛ConcurrentModificationException的异常。

public class TestConcurrent {
	public static void main(String[] args) {
		Collection num = new ArrayList<String>();
		num.add("One");
		num.add("Two");
		num.add("Three");

		Iterator<String> iterator = num.iterator();
		while (iterator.hasNext()) {
			String element = iterator.next();
			if ("One".equals(element)) {
				num.remove(element);
			} else {
				System.out.println(element);
			}
		}
	}
}

运行结果:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at dhp.test.TestConcurrent.main(TestConcurrent.java:16)

源码分析 先来分析下整个代码的执行流程:集合操作中有两个比较重要的指标:modCount和exceptedModCount。   modCount值是用来记录当前集合被修改的次数,每修改一次就进行加1(可以认为为集合的版本号),而exceptedModCount是在iterator初始化是就已经指定的值,值为exceptedModCount = modCount,对于上面的代码,开始就对集合进行了三次add操作,所以modCount=3,当代码执行到第10行时就创建了iterator对象执行exceptedModCount = modCount语句对exceptedModCount进行了赋值操作,此时exceptedModCount=3。具体过程如下:



当执行到while语句时,会对iterator.hasnext()进行判断真假,hasnext访方法实现如下:

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

cursor又是什么呢?cursor是访问集合时指向元素的游标,开始为0,每执行next语句一次就+1,所以当访问到最后一个元素时cursor就等于size,此时就会结束循环。 之后执行next语句,next具体实现如下:

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];
        }

接着代码进入if语句执行了remove操作,modCount的值+1,这时再一次循环while语句进入next语句并执行next里的checkForComodifcation方法对modCount和exceptedModCount的值进行判断,此时modCount!= exceptedModCount就抛出了开始的那个异常,这就是报错的原因。  

当把if语句里的条件改下面代码又会怎样呢?

if("Two".equals(element)){ //条件发生了改变,对第二个元素进行判断
   num.remove(element);
}else {
   System.out.println(element);
}

运行结果:

One

只输出了一个值就结束程序了,并没有报错!   当执行第一遍while语句时,执行了next方法,cursor值+1,cursor=1,并输出One   当执行第二遍while语句时,执行了next方法,cursor值+1,cursor=2,并删除了Two这个元素,size也为2,还记得是怎样判断while条件真假的吗?

public boolean hasNext() {
   return cursor != size;
}
所以当执行第三遍while语句时,cursor = size,hasnext返回false,结束循环,所以程序“来不及”报错就退出循环了,最后只输出一个元素。   

从头到尾来走一遍:

java.util.ConcurrentModificationException
工作中碰到个ConcurrentModificationException。代码如下:
List list = ...;
for(Iterator iter = list.iterator(); iter.hasNext();) {
	Object obj = iter.next();
	...
	if(***) {
		list.remove(obj);
	}
}
在执行了remove方法之后,再去执行循环,iter.next()的时候,报java.util.ConcurrentModificationException(当然,如果remove的是最后一条,就不会再去执行next()操作了)

下面来看一下源码
public interface Iterator<E> {
	boolean hasNext();
	E next();
	void remove();
}

public interface Collection<E> extends Iterable<E> {
	...
	Iterator<E> iterator();
	boolean add(E o);
	boolean remove(Object o);
	...
}
这里有两个remove方法

接下来来看看AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {  
//AbstractCollection和List都继承了Collection
	protected transient int modCount = 0;
	private class Itr implements Iterator<E> {  //内部类Itr
		int cursor = 0;
		int lastRet = -1;
		int expectedModCount = modCount;

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

		public E next() {
			checkForComodification();  //特别注意这个方法
			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;  //重新设置了expectedModCount的值,避免了ConcurrentModificationException的产生
			} catch(IndexOutOfBoundsException e) {
					throw new ConcurrentModificationException();
			}
		}

		final void checkForComodification() {
			if (modCount != expectedModCount)  //当expectedModCount和modCount不相等时,就抛出ConcurrentModificationException
					throw new ConcurrentModificationException();
		}
	}    
}

remove(Object o)在ArrayList中实现如下:
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 void fastRemove(int index) {
	modCount++;  //只增加了modCount
	....
}

所以,产生ConcurrentModificationException的原因就是:
执行remove(Object o)方法之后,modCount和expectedModCount不相等了。然后当代码执行到next()方法时,判断了checkForComodification(),发现两个数值不等,就抛出了该Exception。
要避免这个Exception,就应该使用remove()方法。
这里我们就不看add(Object o)方法了,也是同样的原因,但没有对应的add()方法。一般嘛,就另建一个List了


解决办法 那么又如何在迭代集合时对集合进行操作呢? 方法一: 使用CopyOnWriteArrayList,而不是ArrayList

Collection num = new CopyOnWriteArrayList();

原理: CopyOnWriteArrayList会先复制一个集合副本,当对集合进行修改时普遍的上修改的是副本里的值,修改完后再将原因集合的引用指向这个副本,避免抛出ConcurrentModificationException异常。 底层源码实现如下:

public boolean add(E e) {
   final ReentrantLock lock = this.lock; //重入锁
   lock.lock(); //加锁,效果与synchronized一样
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(); //在finally里解锁是为了避免发生死锁
}
}

方法二: 使用iterator对象的remove方法

if("Two".equals(element)){
    iterator.remove(); //修改了此行
}else {
    System.out.println(element);
}


上面针对ConcurrentModificationException异常在单线程情况下提出了两种解决方案,但是如果涉及到多线程环境可能就不那么乐观了。
下面的例子中开启两个子线程,一个进行遍历,另外一个有条件删除元素:

public class TestConcurrent {
	public static void main(String[] args) {
		final Collection<String> num = new CopyOnWriteArrayList<String>();
		num.add("One");
		num.add("Two");
		num.add("Three");

		/*
		 * Iterator<String> iterator = num.iterator(); while
		 * (iterator.hasNext()) { String element = iterator.next(); if
		 * ("One".equals(element)) { num.remove(element); } else {
		 * System.out.println(element); } }
		 */

		new Thread(new Runnable() {

			@Override
			public void run() {
				for (String string : num) {
					System.out.println("遍历集合 value = " + string);

					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();

		new Thread(new Runnable() {

			@Override
			public void run() {

				for (Iterator<String> it = num.iterator(); it.hasNext();) {
					String value = it.next();

					System.out.println("删除元素 value = " + value);

					if (value.equals("Three")) {
						it.remove();
					}

					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}).start();
		
	}
}

遍历集合 value = One
删除元素 value = One
遍历集合 value = Two
删除元素 value = Two
删除元素 value = Three
遍历集合 value = Three
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
at java.util.ArrayList$Itr.next(ArrayList.java:791)
at dhp.test.TestConcurrent$1.run(TestConcurrent.java:26)
at java.lang.Thread.run(Thread.java:722)

但是方法一是支持多线程的,修改一下两个地方再次测试:





结果如下:

遍历集合 value = One
删除元素 value = One
遍历集合 value = Two
删除元素 value = Two
遍历集合 value = Three
删除元素 value = Three

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值