ArrayList中modCount 和 fail-fast 有什么关系呢?

什么是fail-fast机制

fail-fast 是集合中比较常见的错误监测机制,通常出现在集合的遍历之中。
我们首先可看一下例子:

public class ListTest  {
    public static void main(String[] args) {
        List<String> list  = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        Iterator iterator = list.listIterator();
        while (iterator.hasNext()){
            String s = (String) iterator.next();
            if("b".equals(s)){
                list.remove(s);
            }
        }
    }
}

这段代码竟然执行成功了,不是说,在循环的时候不能删除或者添加元素吗?
我们看下源码:

   public Iterator<E> iterator() {
        return new Itr();
    }
private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

在 ListItr类中维护了一个cursor变量,初始为0。
我们再看hasNext方法

    public boolean hasNext() {
            return cursor != size;
        }

在集合遍历时维护一个初始值为0的游标 cursor,从头到尾进行扫描,当 cursor ==size ,退出遍历。

    public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

每次变量一个元素,cursor都会加1,

在这里插入图片描述
执行remove这个元素后,所有元素往前拷贝,size变为2,这时cursor 也等于2,再执行hasNext() 时,结果为false,退出循环体,并没有机会执行到next()的第一行代码 checkForComodification(),此方法用来判断 expectedModCount 和modCount 是否相等,如果不相等,则抛出 ConcurrentModificationException

上面的例子如果list中多一个元素就会出现异常:

public class ListTest  {
    public static void main(String[] args) {
        List<String> list  = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("e");
        Iterator iterator = list.listIterator();
        while (iterator.hasNext()){
            String s = (String) iterator.next();
            if("b".equals(s)){
                list.remove(s);
            }
        }
    }
}

结果:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.yaspeed2.ListTest.main(ListTest.java:16)

那为什么这时候会出现ConcurrentModificationException异常呢?
因为在删除b后 ,如下图

在这里插入图片描述
cursor = 2,此时 size =3; 再次执行 next 方法 ,此时会执行 checkForComodification();

  final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

expectedModCount 是在

    public Iterator<E> iterator() {
        return new Itr();
    }
   private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;
        }

走到这行代码的时候 Iterator iterator = list.listIterator();
expectedModCount = list.size = 4
但是执行 remove后 modCount 变为3了
再次执行next方法时,expectedModCount != modCount 了 ,所以会报异常!

这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器,即 expectedModCount ,记录已经修改的次数。在进入遍历前,会把实时修改次数modCount赋值给 expectedModCount,如果两个数据不相等,则抛出异常,java.util下的所有集合类都是fail-fast,而concurrent包中的集合类都是fail-safe。

如果想在迭代中删除元素,则可以使用 Iterator进行删除,如果是多线程,则需要加锁

List<String> list  = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("e");
        Iterator iterator = list.listIterator();
        while (iterator.hasNext()){
            String s = (String) iterator.next();
            synchronized (ListTest.class){
                if(s.equals("b")){
                    iterator.remove();
                }
            }
        }

为什么用Iterator 的remove方法就不会出异常呢? 源码如下,因为
修改后的modCount 又重新赋给了 expectedModCount。

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

怎样解决fail-fast

我们可以使用并发容器 CopyOnWriteArrayList 来的代替 ArrayList.

什么是COW呢?

COW 即 Copy-On-Write , 它是并发的一种新思路,实行读写分离。如果是写操作,则复制一个新集合,在新集合内添加或删除元素。当一些修改完成之后,再将原集合的引用指向新的集合。这样做的好处是可以高并发地对COW进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素。

CopyOnWriteArrayList 的 继承关系

在这里插入图片描述

  1. CopyOnWriteArrayList实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。
  2. CopyOnWriteArrayList实现了List,提供了基础的添加、删除、遍历等操作。
  3. CopyOnWriteArrayList实现了RandomAccess,提供了随机访问的能力。
  4. CopyOnWriteArrayList实现了Cloneable,可以被克隆。
  5. CopyOnWriteArrayList实现了Serializable,可以被序列化。

CopyOnWriteArrayList 源码分析

属性

 /** The lock protecting all mutators */
 // 修改时需要加锁
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    // 真正存储元素的地方
    private transient volatile Object[] array;

方法

add(E e) 方法

添加一个元素到末尾

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

执行步骤如下:

  1. 加锁
  2. 获取旧的数组
  3. 新建一个数组,大小为原来长度+1, 并把旧的数据拷贝进去
  4. 添加元素到新数组
  5. 把旧数组的引用指向新数组
add(int index,E element) 方法

在指定你索引处添加元素

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取旧数组
        Object[] elements = getArray();
        int len = elements.length;
        // 检查是否越界, 可以等于len
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            // 如果插入的位置是最后一位
            // 那么拷贝一个n+1的数组, 其前n个元素与旧数组一致
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 如果插入的位置不是最后一位
            // 那么新建一个n+1的数组
            newElements = new Object[len + 1];
            // 拷贝旧数组前index的元素到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将index及其之后的元素往后挪一位拷贝到新数组中
            // 这样正好index位置是空出来的
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 将元素放置在index处
        newElements[index] = element;
        setArray(newElements);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

get(int index)

获取指定的元素,支持随机访问,时间复杂度为O(1)


public E get(int index) {
    // 获取元素不需要加锁
    // 直接返回index位置的元素
    // 这里是没有做越界检查的, 因为数组本身会做越界检查
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

private E get(Object[] a, int index) {
    return (E) a[index];
}

从上面源码可以看到,读取操作是不需要加锁的。

CopyOnWrite的应用场景

CopyOnWrite 并发容器用于读多谢少的并发场景。比如白名单,黑名单等场景。

CopyOnWrite的缺点

1 内存占用问题: 因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

  1. ** 数据一致性问题**。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

总结

(1)CopyOnWriteArrayList使用ReentrantLock重入锁加锁,保证线程安全;
(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

半夏_2021

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

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

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

打赏作者

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

抵扣说明:

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

余额充值