fail-fast
复现异常
List<Integer> list=new ArrayList<>();
for(int i=0;i<5;i++){
list.add(i);
}
Iterator<Integer> iterator=list.iterator();
for(;iterator.hasNext();){
Integer a=iterator.next();
if(a==1){
list.remove(1);
}
}
List<Integer> list=new ArrayList<>();
for(int i=0;i<5;i++){
list.add(i);
}
for(Integer i:list){
if(i==4){
list.remove(i);
}
}
这俩段代码的运行结果,大家可以在自己电脑上面运行一下,它会抛出 java.util.ConcurrentModificationException
什么是 fail fast
fail fast 顾名思义 快速失败,是java集合(Collection)中的一种错误检测机制,在迭代list集合的时候,对 list集合中的元素进行修改例如 :delete add 等,会抛出 java.util.ConcurrentModificationException。为什么会这样呢?
原理
ArrayList 是 AbstractList的子类,而在AbstractList中有一个字段 modCount ,它负责记录list中元素改变的次数。初始值为 0,如下。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
而在创建迭代器时,会初始化一个 exceptedModCount字段,它的初始值为 modCound,在迭代过程中每次都会判断 exceptedModCound 与 modCound是否相同,不同则抛出异常,停止遍历。由源码可得。在文章开头的那段代码中,使用迭代器遍历过程中,我们使用remove 改变了 modCount,导致异常。感兴趣的可以测试一下,除了将 3移除不会由异常,剩下的都会抛出异常
通过源码知原理
迭代器 ----》Array的内部类
public Iterator<E> iterator() {
return new 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; //关键点
Itr() {}
..............
public boolean hasNext() {
return cursor != size; // cursor当前元素的位置
}
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];
}
final void checkForComodification() {
if (modCount != expectedModCount) //关键
throw new ConcurrentModificationException();
}
for each 语句,直接看 target下的class文件
List<Integer> list = new ArrayList();
for(int i = 0; i < 5; ++i) {
list.add(i);
}
Iterator var4 = list.iterator();
while(var4.hasNext()) {
Integer i = (Integer)var4.next();
if (i == 4) {
list.remove(i);
}
}
java 中的 for each 语句最终被优化为 迭代器,所以 它产生异常的原因和第一段代码如
出一辙。那么 当移除3的时候为什么不会抛出异常呢?原因是 当移除 3的时候,
modCount虽然会改变但是由于 当前 size与 cursor相等,会跳出循环,不会进行
modCount 和 exceptedModCount的判断。
解决方法
使用
iterator.remove()
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //更新execptedModCount的值
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
多线程下异常复现
List<Integer> list=new ArrayList<>();
for(int i=0;i<5;i++){
list.add(i);
}
Thread th1=new Thread(){
@Override
public void run() {
Iterator iterator=list.iterator();
while (iterator.hasNext()){
iterator.next();
try {
if((Integer) iterator.next()==1)
iterator.remove();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread th2=new Thread(){
@Override
public void run() {
Iterator iterator=list.iterator();
while (iterator.hasNext()){
iterator.next();
}
}
};
th1.start();
th2.start();
}
众所周知,ArrayList是非线程安全的,多个线程并发访可能导致一些线程安全的问题。上文代码中,俩个线程中的 exceptedModCount未同步更新,导致异常。解决 那肯定是加锁,内部锁不必担心内存泄漏,显示锁灵活强大,但是一定显示放锁,否则将可能导致内存泄露。关于java锁,我会有时间专门写一篇博客介绍。
另一种会抛出异常的情景,不当使用 list.subList()方法
请看代码
List<Integer> list=new ArrayList<>();
for(int i=0;i<5;i++){
list.add(i);
}
List<Integer> l2=list.subList(0,1);
list.removeAll(l2);
System.out.println(l2); //不加不会抛出异常 ------《1》
这段代码会导致异常,当然如果我们在 removeAll后不再使用 l2 ,则这个异常不会有任何影响,但是 如果后面的逻辑中还有 对 l2的操作,则影响甚大。。。,debug时 对于 代码 《1》的描述: Unable to evalute the expression Method threw ‘java.until.ConcurrentMethod Exception’ exception
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
//内部类
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e); //关键之处
this.modCount = parent.modCount;
this.size++;
}
由源码可知,subList并不是创建了一个新的对象,它只是根据传入的参数使用偏移量来对 ArrayList对象操作。当我们改变 调用 子list时,原来的list也会变化,因为 parent.add(parentOffset + index, e);,同理来说对 父list元素的增删也会可能会改变 子list所指向的元素。这时因为改变了 父list,后续对子list的访问可能出现错误,不是我们预期的结果,所以java 的 fail -fast机制会立刻抛出异常,阻止可能的 错误操作
最后关于: subList 大家可以看一下这篇博客