概述
目录
1.问题由来
2.什么是fail-fast机制
3.为什么使用foreach遍历集合删除倒数第二个元素不会报错
4.如何避免出现fail-fast
1.问题由来
阿里开发规范里有一条:不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。这是为什么呢?看下面一个Demo:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
System.out.println(list);
}
当我们使用foreach去遍历集合的同时执行remove(或者add)时,我们可以看到控制台抛出ConcurrentModificationException异常(在删除集合中倒数第二个元素的时候不会抛出此异常,这个问题先埋个坑,我们下面会解释),而我们使用迭代器去remove的时候:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if("1".equals(item)){
iterator.remove();
}
}
System.out.println(list);
}
使用迭代器删除的时候就不会抛出ConcurrentModificationException异常,这是因为如果使用增强for遍历集合时,尝试对集合结构进行改变会触发Java集合的错误检测机制:fail-fast 。
2.什么是fail-fast机制
fail-fast即快速失败,它是Java集合的一种错误检测机制。当多个线程对集合(非fail-safe的集合类)进行结构上的改变的操作时,有可能会触发fail-fast机制,这时就会抛出ConcurrentModificationException(当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常)。注:即使不是多线程环境,如果单线程违反了规则,同样也有可能会抛出改异常。我们再仔细分析一下上面foreach控制台打印的堆栈信息:
两个报错是在ArrayList内部类Itr的next()和checkForComodification()方法,还有一个是笔者的测试类可忽略。我们进入next方法看一下:
可以看到第一行就是这个checkForComodification()方法,我们点进去看一下:
找到抛出异常的位置了,modCount != expectedModCount时,就会抛出ConcurrentModificationException。想搞清楚为什么这两个值会不相等,我们首先要了解这两个变量都是什么,翻阅源码可知:
(1)modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
(2)expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。expectedModCount表示这个迭代器期望该集合被修改的次数。其值是在ArrayList.iterator方法被调用的时候初始化的。只有通过迭代器对集合进行操作,该值才会改变。
我们再看下ArrayList的remove方法:
可以看到ArrayList的remove()只修改了modCount,并没有对expectedModCount做任何操作,add方法同理,在ensureExplicitCapacity()中对modCount进行了修改:
总结:之所以会抛出ConcurrentModificationException异常,是因为我们的代码中使用了增强for去遍历集合,而在增强for底层是通过iterator进行遍历的,但是元素的add()/remove()却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被添加或删除了,就会抛出此异常来提示用户,可能产生了并发修改。
3.为什么使用foreach遍历集合删除倒数第二个元素不会报错
上面foreach遍历集合的那段代码改一下:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
for (String item : list) {
if ("2".equals(item)) {
list.remove(item);
}
}
System.out.println(list);
}
发现竟然不抛异常了,其实这个问题出在迭代器内部类的hasNext()方法上,先看一下迭代器的三个属性:
(1)cursor --迭代器的游标,元素的索引值,初始值为0
(2)lastRet --返回最后一个元素的索引值、如果没有找到则返回-1
(3)expectedModCount --修改次数的期望值,可以看到在迭代器初始化时,这个属性就被赋值为当前修改次数的值了。
在迭代过程中,每一次迭代cursor都会+1,而itr.hasNext()会判断是否存在下一个元素、irt.next()获取下一个元素的值,最终直到不存在下一个元素,则迭代结束。跟进源码发现,itr.hasNext()判断方法并不会调用checkForComodification方法来检查list在迭代中是否有被修改,只是判断游标和长度是否相等,不等时则认为存在下一个元素,只有在调用next()方法才会尝试抛出checkForComodification异常:
现在我们带入删除集合中倒数第二个元素的场景,当倒数第二个元素迭代完成,开始迭代最后一个元素时,此时cursor是2,size由于在迭代过程倒数第二个元素移除了,所以size-1也是2, 此时cursor和size相等,不会再进入下一个迭代,也就不会进入next()方法,因此不会触发checkForComodification方法的fail-fast机制。
4.如何避免出现fail-fast
(1)直接使用普通for循环进行操作
(2)使用Iterator提供的remove方法进行操作
(3)使用一些fail-safe的集合类,例如CopyOnWriteArrayList
最后
以上就是冷静音响为你收集整理的Java中的fail-fast机制的全部内容,希望文章能够帮你解决Java中的fail-fast机制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复