阿里妹导读著述论说了在Java编程中遭受并护士ConcurrentModificationException特别的阅历与训诲骚女qq。 发现问题 在一次开采过程中,业务想知谈音讯是否是退款驱动发送的,而独一判断的次序,是从音讯的明细(可能是乱序的)中取出独一的一个退款明细,通过修改技能,看它是不是终末一条更新的明细,从而判断出它是退款驱动的音讯。初来乍到的笃某不遐想索,对次序入参内的列表使用了排序大法,通过完了compare接口,对应两个Detail的修改技能,便很放浪的获取到了想要的值。次序苟简如下: public Boolean isReFundEvent(Event event){ List<Detail> details = event.getDetails();if(Collections.isEmpty(details)) {thrownew Exception(...); } Collections.sort(details, new Comparator<Detail>() { @Overridepublicint compare(Detail input1, Detail input2) {return input1.getModifiedDate().compareTo(input2.getModifiedDate()); } });return details.get(details.size() - 1).isRefund();} 上述代码看着似乎莫得任何问题,同期还经过了自测、联调以及质料验收等层层关卡,于是代码也铿锵有劲的干预了骨干,部署到了预发,准备通过流量回放这终末的关卡。关系词,此时cdo报了一个很少遭受的特别: ConcurrentModificationException。 碰见特别后,排查起来很容易,苟简的代码如下,不错看到func1在遍历detail处理业务,法子会运行到func3,在阅历了层层签到后,内层的isRefundEvent函数又将正在遍历的details进行了排序,导致了特别的发生。 // 外层劳动类的代码publicvoidfunc1(Event event){ List<Detail> details = event.getDetails();if(Collections.isEmpty(details)) {return;} func2(event);// 不祥其他代码...// 这里对details进行遍历了for(Detail detail : details) { List<FundItem> refundItems = fundService.queryRefundItems(event.getExt().getRefundId());if(!Collections.isEmpty(refundItems)) {for (FundItem refundItem: refundItems) { func3(event, detail, refundItem); } } }}// 中间引擎层的代码publicvoidfunc2(Event event){ isReFundEvent(Event); // 这里也调用了上头那段次序}publicvoidfund3(Event event, Detail detail, FundItem refundItem){// 这里调用到了器具类的代码,也即是上头的那段 isReFundEvent(Event);} 是以在代码实施过程中,迭代器识别到了正在迭代的列表对象在迭代过程中被修改了,抛出了ConcurrentModificationException特别,这种机制称为Fast-Fail,GPT给出的界说为: Fast-fail机制,又称作fail-fast机制,是计较机系统中的一种设想形而上学,它观念当系统检测到差错时应该尽可能快地失败和薪金差错,而不是尝试不绝实施以保捏系统运行。这一形而上学的主旨在于通过赶紧透露问题来简化系统的调试和差错处理,从而幸免差错在系统中膨胀导致更大的毁伤。在软件开采中,fast-fail机制每每诓骗在以下几个方面:1.数据结构和API设想:举例,在Java皆集框架中,迭代器的快速失败行径是一个典型的例子。要是在迭代皆集时,皆集被结构性地修改(添加、删除元素等),迭代器会立即抛出ConcurrentModificationException,而不是尝试处理或忽略这种修改,从而可能产生不成先见的行径。2.参数检查:次序在实施任何操作之前检查其参数的灵验性。要是参数永别法,则立即抛出特别(如IllegalArgumentException),虚假施任何后续操作。3.断言:在代码中使用断言(assert)来考证在某个特定点上的条款是否闲散。要是条款失败,则会抛出一个AssertionError,暗示出现了法子不应该有的景色。4.特别处理:在检测到差错后立即抛出适合的特别,而不是复返一个差错码并期许调用者检查和处理这个差错码。5.事务护士:在数据库操作中,要是事务中的一个操作失败,通盘事务会被坐窝回滚,而不是尝试部分提交。Fast-fail机制不一定在通盘情况下都是渴望的选定。在某些系统中,比如需要高可用性的散播式系统中,可能更稳当给与fail-safe大概fail-soft战略,这些战略在检测到差错时会尝试一些规复大概左迁操作来保管劳动的流畅性。关系词,fast-fail在开采和测试阶段十分有用,它不错匡助开采东谈主员趁早发现并缔造潜在的差错,从而擢升软件的可靠性和珍重性。 特别的旨趣很浅显,使用for轮回对列表对象进行遍历时,编译器会将for轮回优化成迭代器进行遍历,是以径直翻看迭代器的源码不错看到: privateclassItrimplementsIterator<E> {//面前元素索引int cursor; //面前遍历到的元素索引int lastRet = -1; //存ArrayList里面得modCountint expectedModCount = modCount; Itr() {}/* * -- hasNext()次序,判断是否还有元素。 * 因为调用next()时,cursor每次往后转移,当cursor == size时,阐发遍历完了 * (因为cursor是从0启动) */public boolean hasNext(){return cursor != size; }/* * 复返面前元素 */public E next(){//此次序即是去检查modCount的情况。 checkForComodification();//i存储面前将要遍历的元素的索引int i = cursor;//越界检查if (i >= size)thrownew NoSuchElementException();//获取List里面的数组 Object[] elementData = ArrayList.this.elementData;//i大于elementData.length 阐发再次技能数组如故可能发生扩容了,抛特别if (i >= elementData.length)thrownew ConcurrentModificationException();//cursor + 1,指针后移 cursor = i + 1;//复返面前元素。return (E) elementData[lastRet = i]; } } 在迭代器被创建时,会记载面前迭代对象被修改的次数expectedModCount,每当迭代对象(也即是List)被修改时(add、remove、sort等),对象自身的modCount属性都会+1,最终迭代器在获取下个迭代元素前,会调用的checkForComodification次序,通过expectedModCount与modCount进行对比,检查迭代对象是否被修悛改。当两个值不一致时,便会抛出ConcurrentModificationException特别,况兼报错堆栈的位置,亦然for轮回处。 final voidcheckForComodification(){/*检查创建迭代器对象时的modCount与面前modCount是否疏通, *要是不同,阐发面前在迭代遍历元素技能有其他线程对List进行了add大概remove *那么径直抛出特别。 */if (modCount != expectedModCount)thrownew ConcurrentModificationException();} 既然问题如故定位,缔造有狡计亦然十分放浪,对底本要排序的details对象深拷贝后,得到一个副本tempDetail,然后对tempDetail进行排序,好色客偷拍自拍通常不错获胜的获取想要的甩手。关系词,在考证缔造有狡计是否正确时,若何复现特别,酿成了最大的费劲。 四房色播并不是每次都会报错 按照上头的态状,如斯浅显的特别,应该是一个必现问题,因为每次遍历details时,都会进行排序,从而抛出ConcurrentModificationException。 可事实并不是这么,代码通过了通盘的线下的通盘测试,难谈遭受某种“灰电均衡”? 骚女qq 本着严肃、施展的使命格调,笃某又启动了新一轮的排查。 // 外层劳动类的代码publicvoidfunc1(Event event){ List<Detail> details = event.getDetails();if(Collections.isEmpty(details)) {return;} func2(event);// 不祥其他代码...// 这里对details进行遍历了for(Detail detail : details) { List<FundItem> refundItems = fundService.queryRefundItems(event.getExt().getRefundId());if(!Collections.isEmpty(refundItems)) {for (FundItem refundItem: refundItems) { func3(event, detail, refundItem); } } }}// 中间引擎层的代码publicvoidfunc2(Event event){ isReFundEvent(Event); // 这里也调用了上头那段次序}publicvoidfund3(Event event, Detail detail, FundItem refundItem){// 这里调用到了器具类的代码,也即是上头的那段 isReFundEvent(Event);} 不丢脸出,代码其的确实施func2时,法子如故调用过isReFundEvent()了,由于Collections.sort()次序,是径直对传入的元素自己进行排序,是以比及fund3调用时,Event中的details如故是按照修改技能排序好的,最新的一个detail一定会出当今列表的终末一位。 此时,笃某斗胆臆测:是不是迭代器在终末一次遍历时,“悄悄”修改迭代对象,迭代器是不会进行检查的。抱着试一试的心态,笃某怒放了正常作念两数之和的工程文献,写了一个demo: publicstaticvoidmain(String[] args){ ArrayList<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);int cnt = ;for(Integer ele: list) { System.out.println(ele); cnt++;// 这里cnt为1、2、3就会特别,为4时不会抛出特别if (cnt == 4) { Collections.sort(list, new Comparator<Integer>() { @Overridepublicint compare(Integer input1, Integer input2) {return input1 - input2; } }); } } } 现实恶果评释了笃某的臆测,于是再一次的怒放了源码,不错看到: // 终末一次调用时,到这里就终端了public boolean hasNext(){return cursor != size;}/* * 复返面前元素 */public E next(){//此次序即是去检查modCount的情况。 checkForComodification();//i存储面前将要遍历的元素的索引int i = cursor;//越界检查if (i >= size)thrownew NoSuchElementException();//获取List里面的数组 Object[] elementData = ArrayList.this.elementData;//i大于elementData.length 阐发再次技能数组如故可能发生扩容了,抛特别if (i >= elementData.length)thrownew ConcurrentModificationException();//cursor + 1,指针后移 cursor = i + 1;//复返面前元素。return (E) elementData[lastRet = i];} //迭代器遍历过程Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()) { Integer next = iterator.next();// 操作next} 迭代器会在获取下一个元素的时,才会进行modCount的检查,而当迭代器中莫得下一个元素时,会径直间隔迭代,不会走到next次序,也就意味着不去检查迭代器是否被修悛改。 是以,在代码func1中,天然每次遍历details时,惟有代码走到了func3,函数都会对details进行排序,按理说特别详情会发生。然而由于,在func2处,如故对明细进行了排序,导致要是有独一的退款明细,极有可能出当今终末一个(因为大大量往来中,终末的操作都是退款,很少有退款后再支付的场景)。是以实施到func3中的排序代码时,details如故遍历到终末一位了,isReFundEvent函数不错“悄悄”的对明细进行排序,迭代器也不会再进行检查,bug也就铿锵有劲的荫藏了起来。终末,笃某制造了一笔支付后先退款再支付的往来,便告成的将特别复现,修改后的代码也能完好的护士了该问题。 复盘 ConcurrentModificationException特别每每会有单线程和多线程两种可能。 单线程:单线程报错只会是上述情况,存在嵌套在轮回内的皆集类对象自己的修改。提议在写代码的时候,使用对象副本的样式对list等皆集类自己对象进行操作,大概使用迭代器自己自带的remove和add次序进行操作。在大型系统中,由于都是次序之间好多都是高下文传递,次序之间嵌套很深,是以出现该问题的几率照旧很大的,写代码时照旧要自在溯源。 多线程:多个线程同期操作归拢个皆集时,由于arrayList等类是线程不安全的,是以就会出现并发修改特别,提议在多线程操作时,使用线程安全类,大概使用器具类等对皆集进行加锁,再进行修改操作。 要是全国还有更好的护士神色,接待全国在评述区交流。 另外,照旧提议全国严格投诚尺度的研发过程,施展对待质料确立每一个质料卡点,因为它随时可能成为保护你我启动“接雨水”或“两数之和”的终末一关。 冷学问 在排查和考虑过程中,还学到了少量冷学问,沿路共享给全国。 (也不算冷学问,在开采规约中如故阐发过了) 迭代过程中,不错通过迭代器对底本的list进行操作,不要通过list自己。举个例子: 要是径直对list调用remove次序,会报错。 然而调用迭代器自己的remove次序,不会报错: 原因是list的迭代器的remove次序,会将exceptedModCount重置: add次序一样:
|