对比fail-fast和fail-safe
Iterator的安全失败是基于对底层集合做拷贝,因此不受源集合上修改的影响。java.util包下面的集合类在某些条件下回产生快速失败,而java.util.concurrent包下面的所有类都是安全失败的。快速失败的迭代器会抛出ConcurrentModidicationException异常,而安全失败的迭代器永远不会抛出这样的异常。
重点介绍fail-fast产生的原因以及解决办法
1.还原fail-fast实例
fail-fast产生条件:当多个线程对集合进行操作时,若其中一个线程通过Iterator去遍历集合时,该集合的内容被其它线程所改变,就会抛出ConcurrentModificationException异常。
异常代码如下:
package fail_fast;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.junit.Test;
/**
* fail-fast异常的重现
*
* @author cchzao
*
*/
public class TestFailFast {
private List<String> list = new ArrayList<>();
public void printList() {
Iterator<String> ite = list.iterator();
while (ite.hasNext()) {
String str = ite.next();
System.out.println(str + "-");
}
}
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
list.add(String.valueOf(i));
printList();
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 5; i < 10; i++) {
list.add(String.valueOf(i));
printList();
}
}
}
@Test
public void test1(){
new MyThread1().start();
new MyThread2().start();
}
}
输出结果如下:
0-
0-
1-
0-
1-
2-
0-
1-
2-
3-
0-
1-
2-
3-
0-
1-
2-
3-
4-
5-
0-
1-
2-
3-
4-
5-
6-
0-
1-
2-
3-
4-
5-
6-
7-
0-
1-
2-
3-
4-
5-
6-
7-
8-
0-
1-
2-
3-
4-
5-
6-
7-
8-
9-
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
在重现fail-fast实例中,有几次实验发现该异常并没有发生。那是为什么呢?因为fail-fast机制是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。因此在多线程环境中使用fail-fast机制的集合,建议使用“java.util.concurrent”包下的类去取代“java.util”包下的类。
比如可以使用java.util.concurrent包下的CopyOnWriteArrayList类替换ArrayList类。
2.从底层原理上分析为什么ArrayList会发生“java.util.ConcurrentModificationException”
我们关注Iterator是如何获取的:
1.我们的测试实例中这么写:Iterator<String> ite = list.iterator();
2.查看具体的iterator方法
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
//发现,获取迭代器对象的时候,返回了一个Itr对象。
return new Itr();
}
3.进一步查看Itr对象的获取
/**
* 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对象在执行next()、remove()方式时都会执行checkForComodification()方法,这个方法的作用就是查看modCount值和预期值是否一样?如果不一样就报ConcurrentModificationException异常。
这一点我在上上一篇介绍线程同步的时候也说过,当时发现很多线程不安全的方法中都有modCount变量,原来这个变量的作用在此体现出来了。
fail-safe的原因这里就不赘述了,直接翻看CopyOnWriteArrayList源码会发现,这个类底层采用的数组拷贝,自身实现的Iterator,更没有checkForComodification的方法,但是它的线程安全是通过域变量volatile来实现的。