成都传智播客吧 关注:267贴子:2,257
  • 1回复贴,共1

【成都校区】Java并发修改异常的源码解析

只看楼主收藏回复

【传智播客.黑马程序员训练营成都中心】


1楼2018-10-09 14:46回复
    1. 什么时候会产生并发修改异常
    并发的意思是同时发生,那么其实并发修改的字面意思就是同时修改,通过查看JDK的API我们可以得知,并发修改异常的出现的原因是:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
    一个常见的场景就是:当我们在对集合进行迭代操作的时候,如果同时对集合对象中的元素进行某些操作,则容易导致并发修改异常的产生。
    例如我们要完成以下需求:
    在一个存储字符串的集合中,如果存在字符串"Java",则添加一个"Android"
    示范代码如下:
    public class Test { public static void main(String[] args){ ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("Hello"); list.add("World"); Iterator<String> it = list.iterator();//获取迭代器对象 while(it.hasNext()){ //如果迭代器判断集合中还有下一个元素则继续循环 String str = it.next();//获取集合中迭代器所指向的元素 if(str.equals("Java")) {//如果这个元素内容是"Java" list.add("Android");//则在集合中添加一个"Android" } } } }
    控制台输出:
    Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) at java.util.ArrayList$Itr.next(ArrayList.java:831) at com.itheima.day02.Test5.main(Test5.java:17)
    控制台显示的ConcurrentModificationException,即并发修改异常
    下面我们就以ArrayList集合中出现的并发修改异常为例来分析异常产生的原因。
    2. 异常是如何产生的
    2.1 想要知道异常出现的原因,我们需要找到源码中异常出现的根源
    我们能通过控制台找到异常的根源:
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
    异常出现的位置出现在ArrayList类中内部类Itr中的checkForComodification方法
    贴出此方法的源码:
    final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
    由此方法可知,当一个名为modCount的变量值不等于expectedModCount的变量值时,异常对象被抛出。
    2.2 继续探究这两个变量分别是代表什么
    modCount
    modCount是定义在AbstractList抽象类中的public修饰的成员变量,而ArrayList是此类的子类,那么代表ArrayList继承到了modCount这个变量。
    源码中对modCount的解释是:
    The number of times this list has been <i>structurally modified</i>
    - 我们可以理解为:这个变量其实就代表了集合在结构上修改的次数
    - expectedModCount
    - expectedModCount是内部类Itr中的成员变量,当ArrayList对象调用iteroter()方法时,会创建内部类Itr的对象,并给其成员变量expectedModCount赋值为ArrayList对象成员变量的值modCount。
    以下是内部类Itr的部分源码
    private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount; ....
    由此可知,当Itr对象被创建的时候,expectedModCount的值会等于modCount变量的值。
    那么modCount变量在赋值给expectedModCount之前又会如何变化呢?
    当我们创建ArrayList对象的时候,ArrayList对象里包含了此变量modCount并且初始化值为0;
    通过查看源码,我们能发现在ArrayList类中有操作modCount的方法都是添加元素的相关功能和删除元素的相关功能。例如:
    每删除一个元素,modCount的值会自增一次
    public E remove(int index) { rangeCheck(index); modCount++; ...//此处省略代码 E oldValue = elementData(index); return oldValue; }
    在add方法中会调用下面的方法,意味着每添加一个元素,modCount的值也会自增一次
    private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); }
    也就是说:我们每次进行对集合中的元素个数变化的操作时,modCount的值就会+1
    但是这个操作仅限于增删元素,修改元素值并不会影响modCount的值
    再结合API中对此变量的解释,我们可以得出大致的判断:
    其实modCount变量就是记录了对集合元素个数的改变次数
    2.3 分析完这两个关键的变量,我们再结合迭代器的工作流程来分析异常出现的过程
    2.3.1 迭代器的创建
    上文中已经提到过,当ArrayList对象调用iteroter()方法时,会创建内部类Itr的对象。
    此时迭代器对象中有两个最关键的成员变量:cursor、expectedModCount
    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; .....//此处省略下方其他源码 }
    cursor
    迭代器的工作就是将集合中的元素逐个取出,而cursor就是迭代器中用于指向集合中某个元素的指针
    在迭代器迭代的过程中,cursor初始值为0,每次取出一个元素,cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止,从而达到取出所有元素的效果。
    expectedModCount
    expectedModCount在迭代器对象创建时被赋值为modCount
    上文已经分析过,modCount应该理解为集合元素个数的改变次数,或者说结构修改次数
    也就是说,当创建完迭代器对象后,如果我们没有对集合结构进行修改,expectedModCount的值是会等于modCount的值的。
    在迭代集合元素的过程中,迭代器通过检查expectedModCount和modCount的值是否相同,以防止出现并发修改。
    2.3.2 迭代器迭代过程源码分析:
    在2.3.1中我们已经简要的分析过了迭代器工作中最重要的两个变量,下面贴出更多源码结合上文的分析继续说明迭代器是如何工作的。
    我们在使用迭代器的时候,一般会调用迭代器的hasNext()方法判断是否还有下一个元素,此方法源码非常简单:
    public boolean hasNext() { return cursor != size; }
    分析:
    - cursor初始值是0,默认指向集合中第一个元素,每次取出一个元素,cursor值就会自增一次
    - size是集合中的成员变量,用于表示集合的元素个数
    - 因为集合中最后一个元素的索引为size-1,只要cursor值不等于size那么就证明还有下一个元素,此时hasNext方法返回true,如若cursor值与size相等了,那么证明已经迭代完了最后一个元素,此方法返回false。
    - 当我们通过迭代器的hasNext方法返回true值确信集合中还有元素的时候,通常我们会通过迭代器的另一个方法next取出此元素。源码如下:
    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]; } //在next方法的第一行调用了此方法 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
    分析:
    - next()方法第一行就是调用checkForComodification()方法,也就是我们上文中分析过并发修改异常出现根源
    - 当迭代器通过next()方法返回元素之前都会检查集合中的modCount和最初赋值给迭代器的expectedModCount是否相等,如果不等,则抛出并发修改异常。
    - 也就说,当迭代器工作的过程中,不允许集合擅自修改集合结构,如果修改了会导致modCount值变化,从而不会等于expectedModCount,那么迭代器就会抛出并发修改异常。
    - 如果没有异常产生,next()方法最后一行会返回cursor指向的元素。


    2楼2018-10-09 14:47
    回复