Java面试重点_10. Java集合的快速失败机制 “fail-fast”和安全失败机制“fail-safe”
零. 什么是快速失败和安全失败
简而言之:
系统运行中,如果有错误发生,那么系统立即结束,这种设计就是快速失败。
系统运行中,如果有错误发生,系统不会停止运行,它忽略错误(但是会有地方记录下来),继续运行,这种设计就是失败安全。
他们是一种设计思想.
一. “fast-fail” 快速失败机制
- Java的快速失败机制是Java集合框架中的一种错误检测机制,当多个线程同时对集合中的内容进行修改时可能就会抛出
ConcurrentModificationExceptioon
异常. - 其实不仅仅是在多线程状态下,在单线程中用增强 for 循环中一边遍历集合一边修改集合的元素也会抛出
ConcurrentModificationException
异常. 如下:
public class Main{
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for(Integer i : list){
list.remove(i); //运行时抛出ConcurrentModificationException异常
}
}
}
正确的做法是用迭代器的
remove
方法, 便可正常运行.
public class Main{
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
it.remove();
}
}
}
造成这种情况的原因是什么?细心的同学可能已经发现两次调用的 remove() 方法不同,一个带参数据,一个不带参数,这个后面再说,经过查看ArrayList 源码,找到了抛出异常的代码:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
1.1 modCount 和 expectedCount的作用
从上面代码中可以看到如果 modCount 和 expectedModCount 这两个变量不相等就会抛出ConcurrentModificationException 异常。那这两个变量又是什么呢?继续看源码:
protected transient int modCount = 0; //在AbstractList中定义的变量
int expectedModCount = modCount;//在ArrayList中的内部类Itr中定义的变量
从上面代码可以看到, modCount 初始值为0,而 expectedModCount 初始值等于 modCount 。
- 也就是说在遍历的时候直接调用集合的 remove() 方法会导致 modCount 不等于 expectedModCount, 进而抛出
ConcurrentModificationException
异常,而使用迭代器的 remove() 方法则不会出现这种问题。
那么只能在集合的remove() 方法源码找找原因了:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
从上面代码中可以看到只有 modCount++
了,而 expectedModCount
没有操作,当每一次迭代时,迭代器会比较 expectedModCount 和 modCount 的值是否相等,所以在调用 remove() 方法后,modCount 不等于expectedModCount 了,这时就了报 ConcurrentModificationException 异
常。
但用迭代器中 remove() 的方法为什么不抛异常呢?原来迭代器调用的 remove() 方法和上面的 remove() 方法不是同一个!
迭代器调用的 remove() 方法长这样:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; //这行代码保证了expectedModCount和modCount是相等的
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
从上面代码可以看到 expectedModCount = modCount ,所以迭代器的 remove() 方法保证了expectedModCount 和 modCount 是相等的,进而保证了在增强 for 循环中修改集合内容不会报ConcurrentModificationException 异常。
上面介绍的只是
单线程的情况,用迭代器调用 remove() 方法即可正常运行
,但如果是多线程会怎么样呢?
答案是在多线程的情况下即使用了迭代器调用 remove() 方法,还是会报 ConcurrentModificationException 异常
。这又是为什么呢?
还是要从 expectedModCount 和modCount 这两个变量入手分析,刚刚说了 modCount 在 AbstractList 类中定义,而expectedModCount 在 ArrayList 内部类中定义,所以 modCount 是个共享变量而expectedModCount 是属于线程各自的。
简单说,线程1更新了 modCount 和属于自己的expectedModCount ,而在线程2看来只有 modCount 更新了, expectedModCount 并未更新,所以会抛出 ConcurrentModificationException 异常。
二, fail-safe 安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
。
所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException 异常。
缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容。
java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用。
如何边遍历边移除 Collection 中的元素?
从上文“快速失败机制”可知在遍历集合时如果直接调用 remove() 方法会抛出
ConcurrentModificationException 异常,所以使用迭代器中调用 remove() 方法。
拓展文章: 适合细品!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律