在系统设计中,快速失效(fail-fast)系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。
例如:
public int divide(int dividend,int divisor){
if(divisor == 0){
throw new RuntimeException("divisor can't be zero");
}
return dividend/divisor;
}
在以上代码中是一个对两个整数做除法的方法,在divide方法中,我们对除数做了个简单的检查,如果其值为0,那么就直接抛出一个异常,并明确提示异常原因。这其实就是fail-fast理念的实际应用。
那为什么要按设计这个东西呢?
就是可以预先识别出一些错误情况,一方面可以避免执行复杂的其他代码,另外一方面,这种异常情况被识别之后也可以针对性的做一些单独处理。
在Java中,集合类中有用到fail-fast机制进行设计,一旦使用不当,触发fail-fast机制设计的代码,就会发生非预期情况。
在集合类中,为了避免并发修改,会维护一个expectedModCount属性,他表示这个迭代器预期该集合被修改的次数。还有一个modCount属性,他表示该集合实际被修改的次数。在集合被修改时,会去比较modCount和expectedModCount的值,如果不一致,则会触发fail-fast机制,抛出ConcurrentModificationException。
fail-safe 机制是为线程安全的集合准备的,可以避免像 fail-fast 一样在并发使用集合的时候,不断地抛出异常。
集合类中的fail-fast
我们通常说的Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException
ConcurrentModificationException,当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。
在Java中, 如果在foreach 循环里对某些集合元素进行元素的 remove/add 操作的时候,就会触发fail-fast机制,进而抛出ConcurrentModificationException。
例如以下的for-each循环
List<String> userNames = new ArrayList<String>() {{
add("张三");
add("李四");
add("王五");
add("赵七");
}};
for (String userName : userNames) {
if (userName.equals("张三")) {
userNames.remove(userName);
}
}
System.out.println(userNames);
上述代码就是循环遍历list数组,并找到有一个是不是“张三”的名字并删除他,在idea运行时会报以下错误:
对该for-each循环进行反编译得到以下结构:
public static void main(String[] args) {
// 使用ImmutableList初始化一个List
List<String> userNames = new ArrayList<String>() {{
add("张三");
add("李四");
add("王五");
add("赵七");
}};
Iterator iterator = userNames.iterator();
do
{
if(!iterator.hasNext())
break;
String userName = (String)iterator.next();
if(userName.equals("张三"))
userNames.remove(userName);
} while(true);
System.out.println(userNames);
}
可以发现,foreach其实是依赖了while循环和Iterator实现的。
异常原理
通过以上代码的异常堆栈,我们可以跟踪到真正抛出异常的代码是:
再继续跟踪异常代码,可以发现该异常的内部实现
如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不相等,则抛出ConcurrentModificationException。
modCount和expectedModCount是什么?是什么原因导致他们的值不相等的呢?
modCount是ArrayList中的一个成员变量。它表示该集合实际被修改的次数。
例如上述示例代码的前半部分:
List<String> userNames = new ArrayList<String>() {{
add("张三");
add("李四");
add("王五");
add("赵七");
}};
当使用以上代码初始化集合之后该变量就有了。初始值为0。 expectedModCount 是 ArrayList中的一个内部类——Itr中的成员变量。
Iterator iterator = userNames.iterator();
以上代码,即可得到一个 Itr类,该类实现了Iterator接口。 expectedModCount表示这个迭代器预期该集合被修改的次数。其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,该值才会改变。
remove方法的核心逻辑如下:
可以看到,它只修改了modCount,并没有对expectedModCount做任何操作。
其实,上面的这些之所以会抛出ConcurrentModificationException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过iterator迭代器进行的,但是元素的remove却是直接使用的集合类自己的方法。这就导致iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除/添加了,就会抛出一个异常,用来提示用户,可能发生了并发修改! 所以,在使用Java的集合类的时候,如果发生ConcurrentModificationException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。
集合类中的fail-safe
为了避免触发fail-fast机制,导致异常,我们可以使用Java中提供的一些采用了fail-safe机制的集合类。 这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。
例如,可以将上面的ArrayList容器换成CopyOnWriteArrayList,那么程序就不会报错了。
fail-safe集合的所有对集合的修改都是先拷贝一份副本,然后在副本集合上进行的,并不是直接对原集合进行修改。并且这些修改方法,如add/remove都是通过加锁来控制并发的。 所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。(因为fail-fast的主要目的就是识别并发,然后通过异常的方式通知用户) 但是,虽然基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容。如下:
什么是COW
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
CopyOnWriteArrayList相当于线程安全的ArrayList,CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素add到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
注意:CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。也就是说add方法是线程安全的。
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。