本文主要是介绍你了解 ConcurrentModificationException 吗?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见100个问题搞定Java并发
正文
WHY
JDK 中很多容器类在面对复合操作会存在问题,无论在直接迭代还是在 Java 5.0 引人的 for - each 循环语法中,对容器类进行迭代的标准方式都是使用 Iterator
,然而,如果有其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器加锁。
在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是“及时失败”( fail - fast )的。
这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个 ConcurrentModificationException 异常。
WHAT
这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示器。
它们采用的实现方式是,将计数器的变化与容器关联起来:
如果在迭代期间计数器被修改,那么 hasNext 或 next 将抛出 ConcurrentModificationException 。
然而,这种检査是在没有同步的情况下进行的,因此可能会看到失效的计数值,而迭代器可能并没有意识到已经发生了修改。
这是一种设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。
单线程删除
在单线程代码中也可能抛出 ConcurrentModificationException 异常。
当对象直接从容器中删除而不是通过 Iterator.remove 来删除时,就容易抛出这个异常。
错误示例
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test1 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("2".equals(s)) {list.remove(s);}}System.out.println(list);}
}
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at java.util.ArrayList$Itr.next(ArrayList.java:859)at com.shockang.study.java.concurrent.iterator.Test1.main(Test1.java:11)
正确示例
示例 1
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test2 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("1".equals(s)) {list.remove(s);}}System.out.println(list);}
}
[2]
示例 2
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test3 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("2".equals(s)) {list.remove(s);break;}}System.out.println(list);}
}
[1]
示例 3
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class Test4 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");Iterator<String> it = list.iterator();while (it.hasNext()) {String next = it.next();if ("2".equals(next)) {it.remove();}}System.out.println(list);}
}
[1]
示例 4
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test5 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");list.removeIf("2"::equals);System.out.println(list);}
}
[1]
迭代期间加锁
然而,有时候开发人员并不希望在迭代期间对容器加锁。
例如,某些线程在可以访问容器之前,必须等待迭代过程结束,如果容器的规模很大,或者在每个元素上执行操作的时间很长,那么这些线程将长时间等待,同时也可能存在死锁的风险。
关于死锁请参考我的这篇博客——死锁、活锁和饥饿是什么意思?
即使不存在饥饿或者死锁等风险,长时间地对容器加锁也会降低程序的可伸缩性。
持有锁的时间越长,那么在锁上的竞争就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和 CPU 的利用率。
如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器,并在副本上进行迭代。
参考我的这篇博客——一篇文章搞懂 CopyOnWriteArrayList
由于副本被封闭在线程内,因此其他线程不会在选代期间对其进行修改,这样就避免了抛出 ConcurrentModificationException (在克隆过程中仍然需要对容器加锁)。
在克隆容器时存在显著的性能开销。
这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。
隐蔽的迭代器
虽然加锁可以防止迭代器抛出 ConcurrentModificationException ,但你必须要记住在所有对共享容器进行迭代的地方都需要加锁。
实际情况要更加复杂,因为在某些情况下,迭代器会隐藏起来。
容器的 hashCode
和 equals
等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。
同样, containsAll
, removeAll
和 retainAll
等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。
所有这些间接的迭代操作都可能抛出 ConcurrentModificationException 。
这篇关于你了解 ConcurrentModificationException 吗?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!