我目前正在尝试学习如何正确处理对Collections的多线程访问,因此我编写了以下Java应用程序。
正如你所看到的,我创建了一个同步的ArrayList,我尝试从一个Thread中访问一次,一次没有。
我使用for循环迭代ArrayList。为了防止同时对List进行多次访问,我将循环包装到了一个synchronized块中。
public class ThreadTest {
Collection<Integer> data = Collections.synchronizedList(new ArrayList<Integer>());
final int MAX = 999;
/**
* Default constructor
*/
public ThreadTest() {
initData();
startThread();
startCollectionWork();
}
private int getRandom() {
Random randomGenerator = new Random();
return randomGenerator.nextInt(100);
}
private void initData() {
for (int i = 0; i < MAX; i++) {
data.add(getRandom());
}
}
private void startCollectionWork() {
System.out.println("\nStarting to work on data outside of thread");
synchronized (data) {
System.out.println("\nEntered synchronized block outside of thread");
for (int value : data) { // ConcurrentModificationException here!
if (value % 5 == 1) {
System.out.println(value);
data.remove(value);
data.add(value + 1);
} else {
System.out.println("value % 5 = " + value % 5);
}
}
}
System.out.println("Done working on data outside of thread");
}
private void startThread() {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("\nStarting to work on data in a new thread");
synchronized (data) {
System.out.println("\nEntered synchronized block in thread");
for (int value : data) { // ConcurrentModificationException
if (value % 5 == 1) {
System.out.println(value);
data.remove(value);
data.add(value + 1);
} else {
System.out.println("value % 5 = " + value % 5);
}
}
}
System.out.println("Done working on data in a new thread");
}
};
thread.start();
}
}
但每次输入一个for循环时,我都会得到一个ConcurrentModificationException。这是我的控制台输出(每次新运行都会改变):
Starting to work on data outside of thread
Entered synchronized block outside of thread
51
Starting to work on data in a new thread
Entered synchronized block in thread
value % 5 = 2
value % 5 = 2
value % 5 = 4
value % 5 = 3
value % 5 = 2
value % 5 = 2
value % 5 = 0
21
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at ThreadTest.startCollectionWork(ThreadTest.java:50)
at ThreadTest.<init>(ThreadTest.java:32)
at MultiThreadingTest.main(MultiThreadingTest.java:18)
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
at java.util.AbstractList$Itr.next(AbstractList.java:343)
at ThreadTest$1.run(ThreadTest.java:70)
怎么了?
Ps:请不要只发布多线程操作方法的链接,因为我已经阅读了足够的内容。我很好奇为什么我的应用程序没有像我想要的那样运行。
更新:我用明确的Iterator和while循环替换了for(x:y)语法。问题仍然存在..
synchronized(data){
Iterator<Integer> i = data.iterator();
while (i.hasNext()) {
int value = i.next(); // ConcurrentModificationException here!
if (value % 5 == 1) {
System.out.println(value);
i.remove();
data.add(value + 1);
} else {
System.out.println("value % 5 = " + value % 5);
}
}
}
答案 0 :(得分:3)
一旦迭代了一个集合,你就会在迭代的代码块和那个时刻存在的集合之间达成一个契约。合同基本上表明你将按照迭代的顺序获得集合中的每个项目。
问题在于,如果在迭代时修改Collection,则无法维护该合约。集合中的删除将从集合中删除元素,并且可能需要该元素出现在初始迭代中以满足合同。如果在集合中存在元素之前开始的迭代可能检测到元素,则集合中的插入同样会出现问题。
虽然使用多个线程更容易破解此合约,但您可以通过单个线程(如果您选择这样做)破解合同。
通常如何实现这个集合包含一个“版本号”,并且在迭代器抓取集合中的“next”元素之前,它会检查集合的修订版号是否仍然与它时相同迭代器开始了。这只是实现它的一种方式,还有其他方法。
因此,如果您想迭代可能想要更改的内容,则适当的技术是制作集合的副本并迭代该副本。这样您就可以修改原始集合,但不会改变您计划处理的项目的数量,位置和存在。是的,还有其他技术,但从概念上讲,它们都属于“保护您正在迭代的副本,同时更改迭代器无法访问的其他内容”。
答案 1 :(得分:2)
增强的for循环
for (int value : data) {
使用Java Iterator。迭代器是快速失败的,因此如果在Iterator处于活动状态时底层Collection被修改(即通过删除元素),则会得到异常。您的代码会导致对基础Collection的更改:
data.remove(value);
data.add(value + 1);
将代码更改为显式使用java.util.Iterator并使用其remove方法。如果您需要在迭代时向集合中添加元素,则可能需要查看来自java.util.concurrent
包的合适数据结构,例如BlockingQueue
,您可以在其中调用take
方法,该方法将阻止,直到有数据存在;但是可以通过offer
方法添加新对象(非常简单的概述 - Google了解更多)
答案 2 :(得分:2)
出现ConcurrentModificationException
,因为你在迭代它时修改列表......在这种情况下它与多个线程无关......
for (int value : data) { // ConcurrentModificationException here!
if (value % 5 == 1) {
System.out.println(value);
data.remove(value); // you cannot do this
data.add(value + 1); // or that
} else {
System.out.println("value % 5 = " + value % 5);
}
答案 3 :(得分:1)
如果要遍历集合,则只允许通过迭代器从列表中删除项目,因此只能使用一个线程获得ConcurrentModificationException。
更新后的问题更新回复: 在迭代时,不允许向列表中添加元素。
答案 4 :(得分:1)
问题不在于多线程,如果安全的话。但这只是因为你在迭代时修改了集合。
答案 5 :(得分:0)
正如已经指出的那样,你在迭代它时修改了Collection,这会导致你的问题。
将数据更改为List,然后使用常规for循环来跳过它。然后你将不再有迭代器来处理,从而消除你的问题。
List<Integer> data =...
for (int i=0; i<data.size(); i++) {
int value = data.get(i);
if (value % 5 == 1) {
System.out.println(value);
i.remove();
data.add(value + 1);
} else {
System.out.println("value % 5 = " + value % 5);
}
}
答案 6 :(得分:0)
通过在完成迭代的集合周围使用synchronized块,不会消除ConcurrentModificationException。以下一系列步骤发生异常:
为了避免异常,必须确保在任一线程中启动for循环后不修改集合,直到循环结束。