Chinaunix首页 | 论坛 | 博客
  • 博客访问: 425128
  • 博文数量: 23
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 233
  • 用 户 组: 普通用户
  • 注册时间: 2017-11-28 16:33
文章分类

全部博文(23)

文章存档

2020年(3)

2019年(9)

2018年(10)

2017年(1)

我的朋友

分类: Java

2019-04-23 15:46:51

fail-fast:快速实效系统,在系统设计过程中,快速实效系统是一种可以立即报告任何可能表明故障的情况的系统,快速失效系统通常设计于停滞正常工作,而不是试图继续可能存在缺陷的过程,这种设计会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障,快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

其实这是一种设计理念,fail-fast就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报。

一个最简单的fail-fast例子:

点击(此处)折叠或打开

  1. public int divide(int divisor,int dividend){
  2.     if(dividend == 0){
  3.         throw new RuntimeException("dividend can't be null");
  4.     }
  5.     return divisor/dividend;
  6. }

上面的代码是一个对两个整数做除法的方法,在这个方法中,我们对被除数做了一个简单的检查,如果值为0,那么就直接抛出一个异常,并明确提示异常原因,这其实就是fail-fast理念的实际应用。

这样做的好处就是可以预先识别一些错误情况,一方面避免执行其他的复杂代码,另一方面就是在识别到异常情况之后可以做一些简单的处理。

日常会有很多代码都会用到这个机制,但为什么说会让人才坑呢,原因是因为java的部分集合类中,运用了fail-fast的机制进行设计,一旦使用不当,触发了fail-fast机制设计的代码,就会发生非预期的情况。

我们通常所说的java中的fail-fast机制,默认指的是java集合是一种错误检测机制,当多个线程对部分集合进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationExceptionCMException),当方法检测到对象的并发修改,但不允许这种修改时就抛出异常。很多时候正是因为代码中抛出了CMException,很多程序员会很困惑,明明自己的代码没有在多线程环境中使用,为什么还会抛出这种并发相关的异常呢,什么情况下才会抛出呢?

java中,如果在foreach循环里对某些集合元素进行元素的removeadd操作时,就会出发fail-fast机制,进而抛出CMException,如以下代码:

点击(此处)折叠或打开

  1. List<String> userNames = new ArrayList<String>() {{
  2.         add("ll");
  3.         add("yy");
  4.         add("LLYY");
  5.         add("L");
  6.     }};
  7.     for (String userName : userNames) {
  8.         if (userName.equals("ll")) {
  9.             userNames.remove(userName);
  10.         }
  11.     }
  12.     System.out.println(userNames);
  13. }

以上代码,使用增强for循环遍历元素,并尝试删除其中的一个字符串元素,运行以上代码会抛出异常

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 com.hello.operation.FailFastTest.main(FailFastTest.java:21)

同样的,尝试在增强for循环中添加元素也会出现这个异常,在深入理解原理之前,我们尝试先把foreach进行解语法糖,看一下foreach具体如何实现,对编译后的class进行反编译,得到以下代码:

点击(此处)折叠或打开

  1. public static void main(String[] args) {
  2.     // 使用ImmutableList初始化一个List
  3.     List<String> userNames = new ArrayList<String>() {{
  4.         add("ll");
  5.         add("yy");
  6.         add("LLyy");
  7.         add("LL");
  8.     }};

  9.     Iterator iterator = userNames.iterator();
  10.     do
  11.     {
  12.         if(!iterator.hasNext())
  13.             break;
  14.         String userName = (String)iterator.next();
  15.         if(userName.equals("ll"))
  16.             userNames.remove(userName);
  17.     } while(true);

  18.     System.out.println(userNames);
  19. }

可以发现,foreach其实是依赖whileIterator实现的。通过以上代码的异常堆栈,我们可以跟踪到真正抛出异常的代码是:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

该方法是在iterator.next()方法中调用的,看下该方法的实现:

点击(此处)折叠或打开

  1. final void checkForComodification() {
  2.     if (modCount != expectedModCount)
  3.         throw new ConcurrentModificationException();
  4. }

如上,在该方法中对modCount和expectedModCount进行了比较,如果二者不相等,则抛出CMException,那么,modCount和expectedModCount是什么,他们什么时候就会不相等?其实modCount是ArrayList中的一个成员变量,它表示实际该集合被修改的次数。

点击(此处)折叠或打开

  1. List<String> userNames = new ArrayList<String>() {{
  2.     add("ll");
  3.     add("LL");
  4.     add("llyy");
  5.     add("LL");
  6. }}

当使用以上代码初始化集合之后该变量就有值了,初始值为0.

expectedModCount是ArrayList中的一个内部类-Itr中的成员变量。

点击(此处)折叠或打开

  1. Iterator iterator = userNames.iterator();

以上代码,即可得到一个Itr类,该类实现了Iterator接口。expectedModCount表示这个迭代器预期该集合被修改的次数,其值随着Itr被创建而初始化。只有通过迭代器对集合进行操作,其值才会改变。

那么,接下来我们看一下userNames.remove(userName);方法里做了些什么事情,为什么会导致modCount和expectedModCount这两个值不一样,通过翻阅代码我们可以发现,remove方法核心逻辑如下:

点击(此处)折叠或打开

  1. private void fastRemove(int index) {
  2.     modCount++;
  3.     int numMoved = size - index - 1;
  4.     if (numMoved > 0)
  5.         System.arraycopy(elementData, index+1, elementData, index,
  6.                          numMoved);
  7.     elementData[--size] = null; // clear to let GC do its work
  8. }
可以看到,remove方法只修改了modCount,并没有对expectedModCount做任何操作

简单总结一下,之所以会抛出CMException异常,是因为我们的代码中使用了增强for循环,而在增强for循环中,集合遍历是通过Iterator进行的,但是元素的add、remove却是直接使用的集合类自己的方法,这就导致了iterator在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除或者添加了,就会抛出异常,提示用户,可能发生了并发修改。所以,在使用Java的集合类的时候,如果发生CMException,优先考虑fail-fast有关的情况,实际上这里并没有真的发生并发,只是Iterator使用了fail-fast的保护机制,只要他发现有某一次修改是未经过自己进行的,那么就会抛出异常。

为了避免触发fail-fast这个机制,我们可以使用Java中提供的一些采用了fail-fast机制的集合类。Fail-save的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上遍历。

Java.util.concurrent包下的容器都是fail-save的,可以在多线程下并发使用,同时也可以在foreach中进行add、delete。

我们拿CopyOnWriteArrayList这个fail-save的集合类来简单分析一下。

点击(此处)折叠或打开

  1. public static void main(String[] args) {
  2.     //List userNames = new ArrayList()
  3.     List<String> userNames = new CopyOnWriteArrayList<String>(){{
  4.         add("ll");
  5.         add("yy");
  6.         add("LLYY");
  7.         add("L");
  8.     }};
  9.     for (String userName : userNames) {
  10.         if (userName.equals("ll")) {
  11.             userNames.remove(userName);
  12.         }
  13.     }
  14.     System.out.println(userNames);
  15. }

以上代码用CopyOnWriteArrayList代替了ArrayList就不会发生异常。

Fail-save集合的所有对集合的修改都是先拷贝一个副本,然后在副本集合上进行修改,并不是对原集合进行修改,并且这些修改方法,都是通过加锁来控制并发的。所以,CopyOnWriteArrayList中的迭代器在迭代的过程中不需要做fail-fast的并发检测。

但是,虽然基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容。如以下代码:

点击(此处)折叠或打开

  1. public static void main(String[] args) {
  2.     List<String> userNames = new CopyOnWriteArrayList<String>() {{
  3.         add("ll");
  4.         add("llyy");
  5.         add("LLYY");
  6.         add("LL");
  7.     }};

  8.     Iterator it = userNames.iterator();

  9.     for (String userName : userNames) {
  10.         if (userName.equals("llyy")) {
  11.             userNames.remove(userName);
  12.         }
  13.     }

  14.     System.out.println(userNames);

  15.     while(it.hasNext()){
  16.         System.out.println(it.next());
  17.     }
  18. }

迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

在了解了CopyOnWriteArrayList之后,它的add、remove等方法都已经加锁了,还要copy一份再修改干嘛,同样是线程安全的集合,和Vector有什么区别?

Copy-On-write简称COW,是一种用于程序设计的优化策略,其基本思路是,从一开始大家都在共享同一内容,当某人想要修改这个内容时,才会真正把内容Copy出去形成一个新的内容然后再修改,这时一种延时懒惰策略。

CopyOnWrite容器即写时复制容器,通俗的理解就是当我们往一个容器里添加元素时,不直接往当前容器添加,而是现将当前容器进行copy,复制出一个新容器,然后再在新容器里进行操作,完成之后再将原容器的引用指向新的容器。

CopyOnWriteArrayList中add/remove等写方法是需要加锁的,目的是为了避免Copy出N个副本出来,导致并发写。

但是,CopyOnWriteArrayList中的读方法是没有加锁的。

点击(此处)折叠或打开

  1. public E get(int index) {
  2.     return get(getArray(), index);
  3. }

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,当然,这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。

所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。而Vector在读写的时候使用同一个容器,读写互斥,同时只能做一件事儿。


阅读(241206) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~