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 异常。这又是为什么呢?

还是要从 expectedModCountmodCount 这两个变量入手分析,刚刚说了 modCount 在 AbstractList 类中定义,而expectedModCount 在 ArrayList 内部类中定义,所以 modCount 是个共享变量而expectedModCount 是属于线程各自的。

简单说,线程1更新了 modCount 和属于自己的expectedModCount ,而在线程2看来只有 modCount 更新了, expectedModCount 并未更新,所以会抛出 ConcurrentModificationException 异常。

二, fail-safe 安全失败

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会抛出ConcurrentModificationException 异常。

缺点是迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生了修改,迭代器是无法访问到修改后的内容

java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用。

如何边遍历边移除 Collection 中的元素?
从上文“快速失败机制”可知在遍历集合时如果直接调用 remove() 方法会抛出
ConcurrentModificationException 异常,所以使用迭代器中调用 remove() 方法。

拓展文章: 适合细品!

  1. 上篇
  1. 下篇
posted @   青松城  阅读(151)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示