Java集合类的fail-fast机制
作者:互联网
1、前言
什么是fail-fast机制
我们在JDK中科院经常看到类似这样的话
例如 ArrayList
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
上面这段话就很好的给我吗解释了什么叫做快速失败机制。
举例来说就是:假如有两个线程A、B。线程A通过迭代器在遍历集合的元素时候,这时B线程修改了集合的结构。这时程序检测到了线程2对集合的修改了集合的结构,那么这时候程序就会抛出ConcurrentModificationException异常。
这里我们可以写一个简单的demo:
public class FailFastTest {
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
for(int i = 0 ; i < 10;i++){
list.add(i);
}
//这里线程去遍历集合
new threadA(()->{
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
int i = iterator.next();
System.out.println("ThreadA 遍历:" + i);
//因为集合大小只有10个,这里睡眠10毫秒方便测试
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//线程B去修改集合的结构,当i=3 时,修改list(remove)
new threadB(()->{
int i = 0 ;
while(i < 6){
System.out.println("ThreadTwo run:" + i);
if(i == 3){
list.remove(i);
}
i++;
}
}).start();
}
}
OUTPUT:
ThreadA 遍历:0
ThreadB run:0
ThreadB run:1
ThreadB run:2
ThreadB run:3
ThreadB run:4
ThreadB 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)
那么出现ConcurrentModificationException原因是什么。
这里我们可以看看迭代器底层的部分源码
2、深入分析ConcurrentModificationException产生的原因
//这里分享一个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();
}
}
我们可以看到当,ArrayList在调用next、remove方法时都是调用了checkForComodification()这个方法。
这个方法很简单。就是比较modCount是不是期望的expectedModCount,如果不是这丢出ConcurrentModificationException异常。
所以我们就可以推断fail-fast机制产生的原因就是modCount != expectedModCount 的时候产生的。所以我们需知道他们的值在什么时候发生改变
expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量。
那么ModCount什么时候改变了呢?
//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);
}
//修改了modCount
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}
//修改了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;
}
//修改了modCount
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产生的根本原因了。
所以这边我们从实例上去理解expectedModCount 与modCount不一致的过程:
有两个线程(线程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机制。
3、那么什么是fail-safe机制
了解了什么是fail-fast机制,那么fail-safe机制就很好了解了。当我们对集合结构上做出改变的时候,fail-fast机制就会抛出异常。但是,对于采用fail-safe机制来说,就不会抛出异常(大家估计看到safe两个字就知道了)。
这是因为,当集合的结构被改变的时候,fail-safe机制会在复制原集合的一份数据出来,然后在复制的那份数据遍历。=》具体可以看CopyOnWriteArrayList
因此,虽然fail-safe不会抛出异常,但存在以下缺点:
- 复制时需要额外的空间和时间上的开销。
- 不能保证遍历的是最新内容。
4、fail-fast的解决办法
- 在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
- 使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。
5、CopyOnWriteArrayList
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也是相当有损耗的。
6、补充
最后附上阿里巴巴开发手册
【强制】 不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator
方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
我们可以从使用javap -c 查看foreach的class字节码文件
可以发现foreach 循环内部实际是通过 Iterator 实现的,以上代码等同于:
for (Iterator<String> i = list.iterator(); i.hasNext(); ) {
String item = i.next();
if ("1".equals(item)) {
list.remove(item);
}
}
实际上,foreach 循环仅是 Java 提供的语法糖。编译器隐藏了对 Iterator 的使用,使得 foreach 在语法上较传统 for 循环更加简洁,也不容易出错。下面我们看下 Iterator.
我们知道调用 Itr 的 remove() 方法移除集合元素时,首先会调用 ArrayList 的 remove() 方法,再对 expectedModCount 进行更新。在下次调用 Itr.next() 方法获取下个元素时,不会出现 expectedModCount != modCount 的情况。
同时这也解释了(不要在 foreach 循环里进行元素的 remove/add 操作。 remove 元素请使用 Iterator)
标签:Java,ArrayList,list,remove,modCount,线程,fast,fail 来源: https://www.cnblogs.com/linwudi/p/14128496.html