概述
fail-fast的解释
在系统设计中,快速失效是一种可以立即报告任何可能表明故障的机制。快速失效通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这样做的好处是可以预先识别出一些错误情况:
- 一方面可以避免执行复杂的其他代码;
- 另外一方面,这种异常情况被识别之后也可以针对性的做一些单独处理。
Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。当我们使用iterator进行遍历时,使用集合的add/remove修改集合时会报ConcurrentModificationException 异常。此外,调用subList方法获取子list,然后对原list改变,访问子list时也会报该异常。
java fail-fast机制举例
1)场景一:单线程环境下,遍历的时候对集合进行删除
public static void test() {
List<String> list1 = new ArrayList<String>() {
{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}
};
for (String str : list1) {
if ("Hollis".equals(str)) {
list1.remove(str);
}
}
}
报错:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at ListTest.test(ListTest.java:30)
at ListTest.main(ListTest.java:15)
2)场景二:多线程环境下,一个对集合进行遍历元素,一个对集合删除元素
public static void test3() {
List<String> list1 = new ArrayList<String>();
for (int i = 0; i < 1000; i++) {
list1.add(i + "");
}
new Thread() {
@Override
public void run() {
for (String str : list1) {
System.out.println(str);
}
}
}.start();
new Thread() {
@Override
public void run() {
int i = 0;
while (i < list1.size()) {
list1.remove(i);
i++;
}
}
}.start();
}
也会报同样异常。
注:增强型for循环背后的实现就是iterator。
3)场景三:ArrayList 的subList进行操作
详情见:https://blog.csdn.net/liuxiao723846/article/details/104075815
ConcurrentModificationException 异常原理
通过以上代码的异常堆栈,我们可以跟踪到真正抛出异常的代码是:at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)。该方法是在iterator.next()方法中调用的。我们看下该方法的实现:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
该方法中对modCount和expectedModCount进行了比较,如果不等,则抛出CMException。那么,modCount和expectedModCount是什么?是什么原因导致他们的值不想等的呢?
1、modCount:
modCount是ArrayList中的一个成员变量,它表示该集合实际被修改的次数。当初始化集合之后该变量值为0,通过ArrayList的add/remove等方法修改集合时,modCount++。
2、expectedModCount:
expectedModCount 是 ArrayList中的一个内部类——Itr(该类实现了Iterator接口)中的成员变量。其值随着Itr被创建而初始化,expectedModCount表示这个迭代器预期该集合被修改的次数。当通过迭代器对集合进行操作,该值才会改变。
看到这里应该明白了,产生ConcurrentModificationException 异常,是因为代码中使用了增强for循环,而在增强for循环中集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用的集合类自己的方法(修改modCount值)。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了(通过iterator来add/remove会修改expectModCount的值=modCount),就会抛出一个异常,用来提示用户,可能发生了并发修改!
怎么解决fail-fast 出现的异常
1、在遍历元素的同时需要remove操作,需要使用iterator
根据上面分析可知,iterator在remove元素时,会修改expectModCount=modCount,从而避免了该异常。
注:iterator没有add方法。
public static void test4() {
List<String> list1 = new ArrayList<String>() {
{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}
};
Iterator<String> iterator = list1.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("Hollis"))
iterator.remove();
}
System.out.println(list1);
}
总结:不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
2、使用fail-safe容器:
1)fila-safe思想:
java.util.concurrent包下的容器都是fail-safe的,可在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。
这样的集合容器,采用了copy-on-write的思想设计,即对集合的修改都是先拷贝一份副本,然后在副本集合上进行的,并不是直接对原集合进行修改。并且这些修改方法,如add/remove都是通过加锁来控制并发的。所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。(因为fail-fast的主要目的就是识别并发,然后通过异常的方式通知用户)
虽然CopyOnWriteArrayList避免了ConcurrentModificationException,但同样地也会带来一个问题,迭代器并不能访问到修改后的内容。
public static void main(String[] args) {
List<String> userNames = new CopyOnWriteArrayList<String>() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("H");
}};
Iterator it = userNames.iterator();
for (String userName : userNames) {
if (userName.equals("Hollis")) {
userNames.remove(userName);
}
}
System.out.println(userNames);
while(it.hasNext()){
System.out.println(it.next());
}
}
2)Copy-On-Write思想:
大家会不会有这样的疑问:CopyOnWriteArrayList的add/remove等方法都已经加锁了,还要copy一份再修改干嘛?多此一举?同样是线程安全的集合,这玩意和Vector有啥区别呢?
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。但是,CopyOnWriteArrayList中的读方法是没有加锁的,好处是增加读的并发度。所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。
而Vector在读写的时候使用同一个容器,读写互斥,同时只能做一件事儿。
最后
以上就是俊秀白昼为你收集整理的java fail-fast机制的全部内容,希望文章能够帮你解决java fail-fast机制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复