jdk容器-快速失败机制

前两天小伙伴问我关于集合快速失败的问题,勾起了我对快速失败机制 fail-fast 的回忆,这两天也仔细看了一下相关内容,在此做一下总结。

先放出小伙伴提的问题,以及对应的测试代码,我稍后会细讲这个话题——为什么没有快速迭代失败?

fail-fast

先来讲一下 fail-fast,它实际上是一种编程思想,即快速反馈系统错误,防止发生更严重的问题。我们在平时的开发中肯定写过类似的代码,提前判空也属于一种快速失败机制的实现,防止后续出现未处理的空指针异常

    public void submitForm(Info info) {
        if (info == null) {
          // 异常处理逻辑
        }
        // 后续处理逻辑
    }

Arraylist快速失败

先来看一段代码,在遍历 list 的过程中,移除 list 元素。

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (String s : list) {
            System.out.println(s);
            list.remove(s);
        }
        System.out.println(list.size());
    }

运行结果如下:

这边抛出了一个 ConcurrentModificationException 异常,是什么原因导致的异常呢?下面就要提到 Arraylist 的快速失败机制了。

实现原理

将上述测试代码反编译,发现底层使用的是迭代器进行集合遍历。

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            System.out.println(s);
            list.remove(s);
        }

        System.out.println(list.size());
    }

再看到迭代器遍历元素使用的 next() 方法

    private class Itr implements Iterator<E> {
            int cursor;
            int lastRet = -1; 
            int expectedModCount = modCount;
            Itr() {}

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

            @SuppressWarnings("unchecked")
            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];
            }
    }

在遍历前需要检查,看到 checkForComodification() 方法,这里可能会抛出一个 ConcurrentModificationException 异常,依据的是 modCount != expectedModCount 条件

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

这边简单解释一下两个变量:

  • modCount 记录集合的修改次数,任何涉及元素变化的操作都会修改这个值,例如 add, remove
  • expectedModCount,迭代器创建时会用 expectedModCount 记录那个时刻的的修改次数快照,即 expectedModCount = modCount

expectedModCount 的值是静态的,而 modCount 则是动态变化的,这就很好理解上面代码为什么会抛出 ConcurrentModificationException 异常,在遍历集合的过程中,同时修改了集合元素导致 modCount 发生变化,使得 modCount != expectedModCount。

为什么没有快速迭代失败?

既然明白了快速迭代失败的实现机制,再回到开头小伙伴提到的问题,为什么没有快速迭代失败?看到测试代码。

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        for (String s : list) {
            if ("2".equals(s)) {
                list.remove(s);
            }
        }
        System.out.println(list.size());
    }

运行结果:

这边有个很有意思的现象,包括我之前也没有注意到,**如果在遍历集合的过程中,移除倒数第 2 个元素,会发现没有 fail-fast 的异常抛出,**即小伙伴提出的问题。

这就涉及迭代器迭代过程中,边界值的检验问题了。

    while(var2.hasNext()) {
        String s = (String)var2.next();
        if ("2".equals(s)) {
        list.remove(s);
        }
    }

看到 hasNext() 方法代码,它决定是否要继续遍历集合,简单说明 cursor 用来表示下一个即将要遍历到的元素索引,而 size 则是集合当前的元素数量。

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

这就很好理解了,迭代过程如下:

  • 假设遍历到了倒数第二个元素,此时 cursor = size - 1,cursor 从 0 开始计数;

  • 之后进行 list.remove(s),造成 size - 1;

  • 再回到 hasNext() 进行判断,此时 cursor == size 刚好不满足继续遍历的条件,直接退出迭代,也就没有了后续 checkForComodification() 检查

设计思想

上述例子,通过单线程模拟了 ArrayList 存在的线程安全问题,即多线程并发的情况下,可能存在 A 线程正在遍历集合,B 线程同时修改了集合元素的问题,另外通过 fail-fast 机制将安全隐患通知到开发者,下面来看一下 fail-fast 机制的设计思想。

这边引用 jdk ArrayList 源码中的一段话。

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis.
Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs

我们知道 ArrayList 是非同步容器,fail-fast 是为可能发生的并发问题提供一种预警机制,这也是ConcurrentModificationException 异常名称由来,另外既然是预警机制,它并不能保证一定抛出这个异常,只能说尽力去抛出,所以作者也说明了——fail fast 应当用来监控 bug,而不是企图依赖此异常进行其它的程序处理。

在《Java 并发编程实战》一书中就提到,此书的作者包括 ArrayList 源码的开发者 Josh Bloch。

这是一种设计上的平衡,从而降低并发修改操作的检测代码对程序性能带来的影响。

同传统的同步容器 Vector,Hashtable 等不同,ArrayList 实现并没有涉及加锁操作,在保留高性能读写特性的同时,也通过 fail-fast 机制向开发者发出线程安全问题的预警,如此一来,ArrayList 容器的自由度也更高了,让用户自己决定需要不要额外加锁

总结

通过本文,我们主要了解了:

  • 什么是 fail-fast 机制?
  • ArrayList 的快速失败实现
  • 为什么没有快速迭代失败?
  • jdk 容器中 fail-fast 机制的设计思想

在下一篇文章中,我会继续提到另外一种机制——fail-safe,即安全失败机制。如果觉得文章对你有帮助,欢迎留言点赞。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值