- Published on
java中的fail-safe和fail-fast
- Authors
- Name
- Tan Xiang
定义
在系统设计中,快速失效(fail-fast)系统是一种能够快速立即报告任何可能故障的系统。其目的是为了停止正常操作,而不是为了继续执行可能出错的流程。
说白了,就是在做系统设计时,要考虑好异常情况,一但发生可能的异常,立即停止。
在java中,非线程安全的集合类有用到fail-fast机制处理并发操作集合时可能出现的异常情况
非线程安全集合中的fail-fast
在java中,如果在foreach循环里对某些元素进行元素的remove/add操作时,就会触发fail-fast机制,进而抛出ConcurrentModificationException异常
ArrayList<String> userNames = new ArrayList<>() {{
add("tx");
add("txtx");
add("txtxtx");
add("txtxtxtx");
}};
for(String name:userNames){
if(name.equals("tx")){
userNames.remove(name);
}
}
System.out.println(userNames);
对于以上代码,执行后显示如下
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at org.tx.meituan818.main.main(main.java:15)
我们跟踪到真正抛出异常的代码中,
public class ArrayList<E> extends AbstractList<E> ...
private class Itr implements Iterator<E> {
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
该代码中,通过比较modCount和expectedModCount,如果二者不相等,则抛出该异常。再看看这两个变量的定义:
/**
* The number of times this list has been <i>structurally modified</i>.Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results. 此列表在 结构上被修改的次数。结构修改是指更改列表的大小,或以其他方式扰乱列表,使正在进行的迭代可能会产生错误的结果。
* <p>This field is used by the iterator and list iterator implementationreturned by the {@code iterator} and {@code listIterator} methods.If the value of this field changes unexpectedly, the iterator (or listiterator) will throw a {@code ConcurrentModificationException} inresponse to the {@code next}, {@code remove}, {@code previous},{@code set} or {@code add} operations. This provides<i>fail-fast</i> behavior, rather than non-deterministic behavior inthe face of concurrent modification during iteration.此字段由 and listIterator 方法返回iterator的迭代器和列表迭代器实现使用。如果此字段的值意外更改,则迭代器(或列表迭代器)将抛出一个ConcurrentModificationException 以响应 next、 remove、 previousset 或 add 操作。这提供了快速失败的行为,而不是在迭代过程中面对并发修改时的非确 定性行为
* <p><b>Use of this field by subclasses is optional.</b> If a subclasswishes to provide fail-fast iterators (and list iterators), then itmerely has to increment this field in its {@code add(int, E)} and{@code remove(int)} methods (and any other methods that it that result in structural modifications to the list). A single call to{@code add(int, E)} or {@code remove(int)} must add no more than one to this field, or the iterators (and list iterators) will throw bogus {@code ConcurrentModificationExceptions}. If an implementation does not wish to provide fail-fast iterators, this field may be ignored.
子类使用此字段是可选的。 如果一个子类希望提供快速失败的迭代器(和列表迭代器),那么它只需要在其 add(int, E) 和 remove(int) 方法(以及它覆盖的导致对列表进行结构修改的任何其他方法)中增加此字段。对此 add(int, E) 字段的单个调用或 remove(int) 必须添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假 ConcurrentModificationExceptions的 。如果实现不希望提供快速失败迭代器,则可以忽略此字段
*/
protected transient int modCount = 0;
从注释可以得知,该字段需要配合iterator使用,在基于iterator进行remove或者add时,会不会同时维护这两个字段的值?
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();
}
}
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
可以看到,iterator的remove调用了fastremove,然后执行了expectedModCount = modCount;
而在使用userNames.remove(name);
时,直接就执行fastRemove,不会调用这行代码,于是在迭代器进入next时,就会检查到expectedModCount和modCount不相等。
下面再试试用迭代器进行操作:
Iterator<String> iterator = userNames.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
if (name.equals("tx")) {
iterator.remove();
}
}
System.out.println(userNames);
#[txtx, txtxtx, txtxtxtx]
没有报错。
线程安全集合中的fail-safe机制
fail-safe机制使得一些集合类能够安全的进行并发修改,这样的集合容器在遍历时不是直接在集合内容上进行访问,而是先复制原有集合内容,在拷贝的集合上进行操作
缺点是在修改之前获得的迭代器感知不到其变化
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOfRange(o, snapshot, 0, snapshot.length);
return index >= 0 && remove(o, snapshot, index);
}
/**
* A version of remove(Object) using the strong hint that given
* recent snapshot contains o at the given index.
*/
private boolean remove(Object o, Object[] snapshot, int index) {
synchronized (lock) {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i]
&& Objects.equals(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOfRange(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
}
}
可以看到copyOnWriteArrayList的代码,在修改数组时,
- 首先使用
getArray()
方法获取当前的数组snapshot,通过indexofRange遍历找到要删除的元素下标,然后调用实际的remove方法 - 方法中会使用synchronized加锁,确保线程安全,然后拿到current数组,跟snapshot进行比较,如果不等,重新找下标
- 最后使用System.arraycopy将除了要删除的元素的下标位置的其余元素拷贝到新数组里,然后把新数组赋值给array
当然,在修改前获得的迭代器是感知不到其变化的,因为修改后其内部array指向了新的数组引用,而之前的迭代器指向的还是旧数组。
List<String> userNames = new CopyOnWriteArrayList<>() {{
add("tx");
add("txtx");
add("txtxtx");
add("txtxtxtx");
}};
Iterator<String> iterator = userNames.iterator();
for(String name:userNames){
if(name.equals("tx")){
userNames.remove(name);
}
}
System.out.println(userNames);
while(iterator.hasNext()){
System.out.println(iterator.next());
}
Iterator<String> iterator1 = userNames.iterator();
while (iterator1.hasNext()){
System.out.println(iterator1.next());
}
[txtx, txtxtx, txtxtxtx]
tx
txtx
txtxtx
txtxtxtx
txtx
txtxtx
txtxtxtx
vector和CopyOnWriteArrayList的区别是什么?
通过源码可知,vector每个方法上包括读方法都使用了synchronized进行同步,所以其读效率远远不如CopyOnWriteArrayList,并且vector只对原数组进行操作,需要频繁检查是否需要扩容,而CopyOnWriteArrayList不需要扩容,通过COW思想就能使数组容量满足要求。
总结
总结一下,之所以会抛出ConcurrentModificationException,是因为我们的代码中使用了增强for循环,而增强for循环是通过iterator进行的,但是remove或者add确实集合类自己的方法,导致iterator在进入到next时,发现字段被意外修改,于是通过fail-fast机制直接报错。
注:本文中的代码全部基于jdk21