异常案例介绍:

基于C/SJFrame界面,使用JPanel模拟管道,每当生成一个管道,便添加到List中,符合某种条件的时候,删除对应的管道。

算法:遍历List,一旦元素符合便移除,这样剩下的便是符合条件的管道;

wKiom1NIo-6ibcYuAADnTRUKfJY660.jpg

运行结果报异常:ConcurrentModificationException

wKioL1NIo8bzblVTAAJGO42Kx6s195.jpg

查询JDK帮助文档,才发现这种现象属于快速失败迭代器;意思是遍历集合的时候,不允许对集合做更改。

关于ConcurrentModificationException的详细解释,参看以下内容,摘自《JDK API1.6.0

java.util
类 ConcurrentModificationException

java.lang.Object

 java.lang.Throwable

     java.lang.Exception

         java.lang.RuntimeException

java.util.ConcurrentModificationException

所有已实现的接口:

Serializable

public class ConcurrentModificationException

extends RuntimeException

当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。

例如,某个线程在 Collection 上进行迭代时,通常不允许另一个线性修改该 Collection。通常在这些情况下,迭代的结果是不确定的。如果检测到这种行为,一些迭代器实现(包括 JRE 提供的所有通用 collection 实现)可能选择抛出此异常。执行该操作的迭代器称为快速失败迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险。

注意,此异常不会始终指出对象已经由不同 线程并发修改。如果单线程发出违反对象协定的方法调用序列,则该对象可能抛出此异常。例如,如果线程使用快速失败迭代器在 collection 上迭代时直接修改该 collection,则迭代器将抛出此异常。

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败操作会尽最大努力抛出 ConcurrentModificationException。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。

下面来谈如何解决!

解决方案之一:用另一个集合(比如List)记录要删除的元素信息,待遍历完成之后,进行删除操作;

wKioL1NIo8bwK3GuAAGd5-W8b5k076.jpg

这样,结果是正确的!

wKioL1NIo8ayUyi_AAEtUpfA7WQ819.jpg

解决方案之二:使用迭代器Iterator,进行元素的删除操作;

wKiom1NIo--AgLb7AAElB5QhzEc558.jpg

这样写,我想应该更合理吧!

wKioL1NIo8bwyWSbAAFHHHHI0vA256.jpg

二者结果完全一样的!

wKiom1NIo--wQE3WAAE5VASKSGM257.jpg

解决方案之三:使用CopyOnWriteArrayList进行元素的删除操作,它是ArrayList的一个线程安全的变体,其中所有可变操作(addset 等等)都是通过对底层数组进行一次新的复制来实现的;

前提:CopyOnWriteArrayList替换掉ArrayList

wKiom1NIpWjxD9-_AAB2SDs7sLg059.jpg


wKiom1NIo_TAUZquAAEHEoTBJV4343.jpg

结果正确,方案可行!

wKioL1NIo8yAGAxwAAEgd5tXPzk991.jpg

关于CopyOnWriteArrayList的详细解释,参看以下内容,摘自《JDK API1.6.0

java.util.concurrent
类 CopyOnWriteArrayList<E>

java.lang.Object
java.util.concurrent.CopyOnWriteArrayList<E>

类型参数:

E - collection 中所保存元素的类型

所有已实现的接口:

Serializable, Cloneable, Iterable<E>,Collection<E>, List<E>, RandomAccess

public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable

ArrayList的一个线程安全的变体,其中所有可变操作(addset等等)都是通过对底层数组进行一次新的复制来实现的。

这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(removesetadd)不受支持。这些方法将抛出 UnsupportedOperationException。允许使用所有元素,包括 null

内存一致性效果:当存在其他并发 collection 时,将对象放入 CopyOnWriteArrayList之前的线程中的操作 happen-before随后通过另一线程从 CopyOnWriteArrayList中访问或移除该元素的操作。

解决方案之四:如果确定每次遍历只删除一个元素,使用break语句跳转,从而实现元素的删除操作;

wKiom1NIo_XxKsfGAAEfGapOz34842.jpg

由于这种方法有特殊的条件环境,属于“自作聪明”的方法,不适应本案例(结果是不正确的),但不失为一种方法。

wKioL1NIo8yRvzmZAAE845Ls9II730.jpg

总结:以上四种方法,各有各的好处,同时也有不足,这就看你对程序的性能要求了,我的此程序用了多线程,因此首选第二种。PS:其实由于现在的硬件配置,再加上编译器对代码的优化,小程序几乎是比较不出来差别的,所以解决问题、积累经验是王道。

以上难免有不足之处,还望大家批评指正。

共同学习,共同成长!