控制对 Java 集合的并发访问的最佳方法

Posted

技术标签:

【中文标题】控制对 Java 集合的并发访问的最佳方法【英文标题】:Best way to control concurrent access to Java collections 【发布时间】:2010-10-08 09:08:50 【问题描述】:

我应该使用旧的同步向量集合、具有同步访问的 ArrayList 还是 Collections.synchronizedList 或其他一些用于并发访问的解决方案?

我在相关问题和搜索中都没有看到我的问题(Make your collections thread-safe? 不一样)。

最近,我不得不对我们应用程序的 GUI 部分进行某种单元测试(主要是使用 API 来创建框架、添加对象等)。 由于这些操作的调用速度比用户调用的快得​​多,因此在尝试访问尚未创建或已删除的资源的方法方面存在许多问题。

在 EDT 中发生的一个特定问题来自于遍历视图的链接列表,同时在另一个线程中更改它(在其他问题中遇到 ConcurrentModificationException)。 不要问我为什么它是一个链表而不是一个简单的数组列表(甚至更少,因为我们通常在里面有 0 或 1 个视图......),所以我在我的问题中采用了更常见的 ArrayList(因为它有一个表弟)。

无论如何,我对并发问题不太熟悉,我查了一些信息,想知道在旧的(并且可能已经过时的)Vector(它在设计上具有同步操作)和带有synchronized (myList) 的 ArrayList 之间选择什么关键部分(添加/删除/遍历操作)或使用 Collections.synchronizedList 返回的列表(甚至不确定如何使用后者)。

我最终选择了第二个选项,因为另一个设计错误是暴露对象(getViewList() 方法...)而不是提供使用它的机制。

但是其他方法的优缺点是什么?


[编辑] 这里有很多好的建议,很难选择一个。我会选择更详细并提供链接/思想的食物... :-) 我也喜欢 Darron 的。

总结一下:

正如我所怀疑的那样,Vector(以及它的邪恶孪生兄弟,可能还有 Hashtable)在很大程度上已经过时了,我看到人们说它的旧设计不如新集合,甚至在强制同步的缓慢性之外单线程环境。如果我们保留它,主要是因为旧的库(和部分 Java API)仍在使用它。 与我所想的不同,Collections.synchronizedXxxx 并不比 Vector 更现代(它们似乎与 Collections 是同时代的,即 Java 1.2!),实际上并没有更好。很高兴知道。简而言之,我也应该避免使用它们。 毕竟手动同步似乎是一个很好的解决方案。可能存在性能问题,但在我的情况下,这并不重要:对用户操作执行的操作、少量集合、不经常使用。 java.util.concurrent 包值得牢记,尤其是 CopyOnWrite 方法。

我希望我做对了... :-)

【问题讨论】:

另见***.com/questions/510632/… 【参考方案1】:

Vector 和 Collections.synchronizedList() 返回的 List 在道德上是一回事。我认为 Vector 被有效地(但实际上不是)弃用,并且总是更喜欢同步列表。一个例外是需要 Vector 的旧 API(尤其是 JDK 中的 API)。

使用裸 ArrayList 并独立同步使您有机会更精确地调整同步(通过在互斥块中包含其他操作或将多个对 List 的调用放在一个原子操作中)。不利的一面是,可以编写在同步之外访问裸 ArrayList 的代码,这是被破坏的。

您可能要考虑的另一个选项是 CopyOnWriteArrayList,它可以像 Vector 和同步 ArrayList 一样为您提供线程安全,但也可以提供不会抛出 ConcurrentModificationException 的迭代器,因为它们正在处理数据的非实时快照。

您可能会发现最近关于这些主题的一些博客很有趣:

Java Concurrency Bugs #3 - atomic + atomic != atomic Java Concurrency Bugs #4: ConcurrentModificationException CopyOnWriteArrayList concurrency fun

【讨论】:

【参考方案2】:

我强烈推荐《Java Concurrency in Practice》这本书。

每个选择都有优点/缺点:

    向量 - 被认为是“过时的”。与更主流的合集相比,它获得的关注和错误修复可能更少。 您自己的同步块 - 很容易出错。通常提供比以下选择更差的性能。 Collections.synchronizedList() - 由专家完成的选择 2。这仍然不完整,因为需要原子的多步操作(get/modify/set 或迭代)。 来自 java.util.concurrent 的新类 - 通常具有比选择 3 更有效的算法。关于多步骤操作的类似警告适用,但通常会提供帮助您的工具。

【讨论】:

【参考方案3】:

我想不出比ArrayList 更喜欢Vector 的充分理由。 Vector 上的列表操作是同步的,这意味着多个线程可以安全地更改它。就像你说的,ArrayList 的操作可以使用Collections.synchronizedList 同步。

请记住,即使使用同步列表,如果在另一个线程修改集合时迭代集合,您仍然会遇到ConcurrentModificationExceptions。因此,在外部协调访问非常重要(您的第二种选择)。

用于避免迭代问题的一种常用技术是迭代集合的不可变副本。见Collections.unmodifiableList

【讨论】:

【参考方案4】:

我总是先去 java.util.concurrent (http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html) 包,看看有没有合适的集合。 java.util.concurrent.CopyOnWriteArrayList 类如果您对列表进行少量更改但需要更多迭代,则该类很好。

我也不相信 Vector 和 Collections.synchronizedList 会阻止 ConcurrentModificationException。

如果您没有找到合适的集合,那么您必须自己进行同步,如果您不想在迭代时持有锁,您可能需要考虑制作一个副本并迭代该副本。

【讨论】:

【参考方案5】:

我不相信 Vector 返回的 Iterator 以任何方式同步 - 这意味着 Vector 不能保证(就它自己)一个线程没有修改底层集合,而另一个线程正在迭代它。

我相信要确保迭代是线程安全的,您必须自己处理同步。假设同一个 Vector/List/object 被多个线程共享(从而导致您的问题),您不能只在该对象本身上进行同步吗?

【讨论】:

【参考方案6】:

最安全的解决方案是完全避免并发访问共享数据。不要让非 EDT 线程对相同的数据进行操作,而是让它们调用 SwingUtilities.invokeLater() 并使用 Runnable 执行修改。

说真的,共享数据并发是毒蛇的巢穴,您永远不会知道某个地方是否隐藏着另一个竞争条件或死锁,等待时机在最坏的情况下咬您一口。

【讨论】:

【参考方案7】:

CopyOnWriteArrayList 值得一看。它专为通常从中读取的列表而设计。每次写入都会导致它在幕后创建一个新数组,因此那些遍历数组的人不会得到 ConcurrentModificationException

【讨论】:

以上是关于控制对 Java 集合的并发访问的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

java--Iterator迭代问题:集合并发访问异常

Java笔记:并发工具

Java开发之高并发编程篇——安全访问的集合

操作集合的工具类 Collections

Java软件开发 | 高并发编程篇之——安全访问的集合

并发集合(转)