我得到一个java.util.ConcurrentModificationException
。
相关代码为:
for (Iterator<Audit> it = this.pendingAudits.iterator(); it.hasNext();) {
// Do something
it.remove();
}
调用it.remove()
时会抛出此异常。
此代码在@Service
带注释的类中:
@Service
public class AuditService {
public AuditService(
this.pendingAudits = new ArrayList<Audit>();
}
public void flush(Audit... audits) {
this.pendingAudits.addAll(Arrays.asList(audits));
try {
for (Iterator<Audit> it = this.pendingAudits.iterator(); it.hasNext();) {
// Do something
it.remove();
}
}
}
}
当两个请求到达代码时,就会出现问题。
如何避免这种并发访问异常?
答案 0 :(得分:3)
首先,这不是与Spring相关的问题。并发修改一个不太并行的ArrayList
类只是一个问题。
最简单的解决方案是将对修改它的方法的访问同步。
public synchronized void flush(Audit... audits) { }
请记住,它将强制执行flush
方法的顺序执行,这会带来巨大的性能损失。
旁注,仅使用Collections.synchronizedList
来同步集合本身是不够的-同步包装返回的迭代器实例需要手动同步。
答案 1 :(得分:0)
不是很明显吗?您正在共享数据而没有正确的同步。
@Service带注释的类通常是单例作用域类,因此,所有调用线程之间将共享同一实例。
这导致所有线程在同一实例上访问flush方法。
你猜怎么着?
您的flush方法正在尝试修改作为成员变量的ArrayList。这使其在多线程方案中不安全。
现在是重新访问ArrayList文档的好时机,它介绍了有关迭代器的更多信息。
请注意,此实现未同步。如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素或显式调整后备数组大小的任何操作;仅设置元素的值不是结构修改。)这通常是通过对自然封装了对象的某些对象进行同步来实现的。清单。如果不存在这样的对象,则应使用Collections.synchronizedList方法“包装”列表。最好在创建时完成此操作,以防止意外的不同步访问列表: 列表列表= Collections.synchronizedList(new ArrayList(...)); 此类的迭代器和listIterator方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时间以任何方式对列表进行结构修改,除非通过迭代器自己的remove或add方法,否则迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速干净地失败,而不会在未来的不确定时间内冒任意,不确定的行为的风险。 请注意,迭代器的快速失败行为无法得到保证,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应仅用于检测错误。
答案 2 :(得分:0)
我认为您可能将Audit放在某个地方的auditAudits列表中,并且想要在调用flush(Audit ...)方法时将其全部刷新,可以使用ConcurrentLinkedQueue而不是ArrayList。
public AuditService(
this.pendingAudits = new ConcurrentLinkedQueue<Audit>();
}
public void flush(Audit... audits) {
this.pendingAudits.addAll(Arrays.asList(audits));
try {
Audit audit;
while ((audit = this.pendingAudits.poll) != null) {
// Do something
}
}
}