集合类你不知道的小知识

在系统设计中,快速失效(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实现的。

异常原理

通过以上代码的异常堆栈,我们可以跟踪到真正抛出异常的代码是:

再继续跟踪异常代码,可以发现该异常的内部实现

如上,在该方法中对modCountexpectedModCount进行了比较,如果二者不相等,则抛出ConcurrentModificationException。

modCountexpectedModCount是什么?是什么原因导致他们的值不相等的呢?  

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并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dlwlrma-IU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值