从jdk1.8源码看modCount的作用

上一篇《fail-fast究竟是个什么鬼》我们学习了什么是fail-fast,那么,java.util包下的集合类是如何利用modCount保证fail-fast机制的?那么modCount作为java.util包中的灵魂字段,我觉得就很有必要专门为它写上一篇。接下来,本文将以最常用的ArrayList类作为源码切入点,一起来揭秘modCount的神秘面纱。

先看一下ArrayList的继承实现关系图(idea快捷键 Ctrl + Alt + u 或 Ctrl + Alt + Shift + u )。
在这里插入图片描述

在关系图中可以看到,ArrayList继承了AbstractList,在AbstractList中有一个modCount成员变量。

    /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

源码中的注释才是最好的一手材料。基于源码写出来的材料,会因为作者的能力而良莠不齐,同样,也会因读者的能力导致这种知识的传递效果迥异。先看一下注释:

这个列表在结构上被修改的次数 。 结构修改是那些改变列表大小的修改,或者以一种方式(这种方式使得进行中的迭代可能产生不正确的结果)扰乱列表。

这个字段被迭代器和列表迭代器的实现所使用,这种实现的实例是通过iterator方法和listIterator方法返回的。如果这个字段的值意外地改变了,那么迭代器(或列表迭代器)在响应next,remove,previous,set或者add操作时将会抛出ConcurrentModificationException。在迭代过程中面对并发修改,这就提供了**fail-fast(快速失败)**行为,而不是非确定的行为。

子类使用这个字段是可选择的。如果子类希望提供fail-fast迭代器(或列表迭代器),那么在add方法和remove方法中(或者该子类重写的任何方法,这些方法会导致列表的结构修改)就不得不增加这个字段的值。单次调用add方法或remove方法必须为这个字段加1,否则迭代器(或列表迭代器)将会抛出虚假的ConcurrentModificationExceptions。如果一种实现不希望提供fail-fast迭代器,那么这个字段就会被忽略。

modCount的使用范围修饰符是protected,所以子类会继承它。transient表示该字段不需要序列化而已。modCount意思是被修改的次数,应该是modifiedCount的缩写。

对于上面的翻译,做更进一步理解。

structurally modified

structurally modified 结构性修改,在一个集合(Collection接口)中什么样的修改,可以称为结构性修改。结构的修改是指改变列表的size的修改,那size是什么?size是一个集合中所含的元素个数,在Collection接口中有个size() 方法就是返回集合的元素个数。

    /**
     * Returns the number of elements in this collection.  If this collection
     * contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
     * <tt>Integer.MAX_VALUE</tt>.
     *
     * @return the number of elements in this collection
     */
    int size();

那么什么方法会改变一个集合实现类的size呢?当然,是add或remove(以及add和remove衍生的方法)。也是就集合实现类,会在add或remove方法中,会执行modCount++。可以看看ArrayList的add和remove源码:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

	public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

add方法的ensureCapacityInternal(size + 1),后面的注释Increments modCount!!,就是说会自增modCount。remove方法中可以清楚的看到有modCount++(其它add和remove的衍生方法addAll,removeAll也会同样自增modCount)。

同时,注意到后面还有一种关于结构性修改的解释——以一种方式扰乱列表,这种方式会使得进行中的迭代可能产生不正确的结果。从这句话中,我理解到的是这种方式没有改变size的大小,而且这种方式区别于前面说的改变列表size大小的方式,那么,什么方式会在不改变列表size的大小的情况下,会使得迭代过程可能产生不正确的结果 ?说实话,这个问题我想很久没想明白。直到有一天,我看到源码的时一眼扫到了sort方法。刹那间,茅塞顿开——如果一个线程A在迭代输出一个含有1,2,3的ArrayList,另外一个线程B在线程A的迭代的过程中,将ArrayList倒序排序了,那么线程A可能会输出1,2,1,这就造成了size的大小不变,而迭代过程产生了不正确的结果。那么,sort方法中肯定有modCount++。

    @Override
    @SuppressWarnings("unchecked")
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;
        Arrays.sort((E[]) elementData, 0, size, c);
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

同时,属于第二种方式的还有replaceAll方法,retainAll方法,removeIf方法等等。

注意到,在LinkedList的源码注释中,有这样一句话“A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.”。结构性修改是指任何会添加或删除一个或多个元素的操作,仅仅设置一个元素的值不是结构性的修改。因为LinkedList源码中没有ArrayList的第二种方式的那些方法。

changes unexpectedly

changes unexpectedly 意外地改变,是指在使用迭代器和列表迭代器时,这个list的size被改变了,而这种改变不是通过迭代器自己的方法。那么迭代器在响应自身的方法(next,remove,previous,set或者add),会报错ConcurrentModificationException。这句话的意思是,在使用迭代器时要想进行结构性修改必须使用迭代器自身的方法,如果不是,迭代器就认为是发生了意外地改变,就会报错。在也就是明明单线程中,在迭代过程中使用列表的remove方法,没有使用迭代器remove方法会报错ConcurrentModificationException的原因。

在ArrayList的有两个内部类Itr和ListItr,在这两个内部类中有next,remove,previous,set、add方法,这些方法都调用checkForComodification()方法。checkForComodification就是检查是否发生并发修改的方法,其实现原理就是比较modCount和expectedModCount是否相等,如果相等则不是则认为发生了ConcurrentModificationException。

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

那expectedModCount(被期望的修改次数)是什么意思?是哪个类的字段?在内部类Itr中,有这样一行代码"int expectedModCount = modCount;",也就是说expectedModCount初始化的时候是取得modCount值。从上面的ArrayList的add和remove源码中,我们知道了modCount会自增,而没有expectedModCount的自增,那么在迭代过程中使用了ArrayList的add和remove方法当然会报错。与此同时,我们注意到Itr也有remove方法,那么在迭代过程中,调用Itr.remove()不会报错,是不是意味着在Itr.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();
            }
        }

可以清楚的看到“expectedModCount = modCount;”,Itr.remove()方法try语法块中,先调用了ArrayList.this.remove(lastRet)也就是ArrayList的remove方法,modCount会自增。接着执行“ expectedModCount = modCount;”,这样就会将重置expectedModCount ,使得expectedModCount 和modCount保持一致。这样一来,就不会发生ConcurrentModificationException。

fail-fast

维基百科中关于fail-fast的解释:

在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。
其实,这是一种理念,fail-fast就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报

更多关于fail-fast的内容可以查看《fail-fast究竟是个什么鬼》

同样地,Set和Map的实现类也会有fail-fast机制,实现原理和List基本相同。java.util包下的集合实现类和Map实现类都实现了fail-fast机制。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值