题 对集合进行线程安全迭代


我们都知道什么时候使用 Collections.synchronizedXXX (例如。 synchronizedSet())我们获得了底层集合的同步“视图”。

但是,这些包装器生成方法的文档 声明我们必须明确地同步集合 使用迭代器迭代集合时。

您选择哪个选项来解决此问题?

我只能看到以下方法:

  1. 按照文档说明:对集合进行同步
  2. 在致电之前克隆该集合 iterator()
  3. 使用迭代器是线程安全的集合(我只知道 CopyOnWriteArrayList/组)

并且作为一个额外的问题:当使用同步视图时 - 使用foreach / Iterable线程安全吗?


18
2017-12-23 10:00


起源


或者可以通过使用ExecutorService等来避免共享使用迭代器(使用迭代器将新的Callable / Runnable添加到ExecutorService)。其他一些集合类型将自己称为“线程安全”,我想知道它们的迭代器是否存在:stackoverflow.com/a/3362063/32453(我怀疑它不知何故) - rogerdpack


答案:


你已经真的回答了你的奖金问题:不,使用增强的for循环  安全 - 因为它使用迭代器。

至于哪种方法最合适 - 它实际上取决于你的背景:

  • 写作很少见吗?如果是这样, CopyOnWriteArrayList 可能是最合适的。
  • 集合相当小,迭代速度快吗? (即你在循环中没有做太多的工作)如果是这样的话,同步可能会很好 - 特别是如果这种情况不常发生(即你不会对集合争用很多)。
  • 如果你正在做很多工作并且不想阻止其他线程同时工作,那么克隆集合的命中可能是可以接受的。

25
2017-12-23 10:15



是否保证foreach将始终使用迭代器()或者这是特定于实现的? - MRalwasser
@MRalwasser:除了只使用数组索引的数组外,它由规范保证。见JLS第14.14.2节: java.sun.com/docs/books/jls/third_edition/html/... - Jon Skeet


取决于您的访问模式。如果您具有较低的并发性和频繁写入,则1将具有最佳性能。如果您具有高并发和不频繁的写入,则3将具有最佳性能。选项2几乎在所有情况下都会表现不佳。

foreach 电话 iterator(),所以完全相同的事情适用。


6
2017-12-23 10:18





您可以使用Java 5.0中添加的一个较新的集合,它在迭代时支持并发访问。另一种方法是使用toArray复制一个线程安全的副本(在复制期间)。

Collection<String> words = ...
// enhanced for loop over an array.
for(String word: words.toArray(new String[0])) {

}

4
2017-12-23 13:39





我可能完全不满足您的要求,但如果您不了解它们,请查看 谷歌的集合 考虑到“不容置疑”。


1
2017-12-23 10:48





我建议下降 Collections.synchronizedXXX 并在客户端代码中统一处理所有锁定。基本集合不支持在线程代码中使用的复合操作,即使您使用 java.util.concurrent.* 代码更难。我建议保留尽可能多的代码与线程无关。保持困难且易出错的线程安全(如果我们非常幸运)代码至少。


1
2017-12-23 13:06





您的所有三个选项都可以使用。根据您的情况选择合适的产品取决于您的具体情况。

CopyOnWriteArrayList 如果你想要一个列表实现,并且你不介意每次写入时都复制底层存储,它将起作用。只要您没有非常大的集合,这对性能非常有用。

ConcurrentHashMap 要么 ”ConcurrentHashSet“(使用 Collections.newSetFromMap)如果你需要,它会工作 Map 要么 Set 接口,显然你不会以这种方式获得随机访问。一个伟大的!关于这两个问题的一点是,它们可以很好地处理大型数据集 - 当发生变异时,它们只会复制底层数据存储的一小部分。


1
2018-04-01 10:29





它取决于实现克隆/复制/ toArray(),新的ArrayList(..)和喜欢获取快照的结果。  锁定集合。 使用synchronized(集合)和迭代来确保在迭代结束时不会修改,即有效地锁定它。

旁注:(当内部需要创建临时ArrayList时,toArray()通常是首选的,但有一些例外。另请注意,除了toArray()之外的任何东西都应该包含在synchronized(集合)中,使用Collections.synchronizedXXX提供。


0
2018-01-03 22:26





这个问题相当陈旧(对不起,我有点迟了......)但我还想加我的答案。

我会选择你的第二个选择(即在调用iterator()之前克隆该集合)但是有一个重大的转折。

假设,你想使用迭代器进行迭代,你不必在调用.iterator()之前使用Coppy,而不是使用“松散地使用”这个术语“忽略”迭代模式的概念,但你可以编写一个“ThreadSafeIterator”。

它可以在同一个前提下工作,协调Collection,但不要让迭代类知道,你就是这么做的。这样的迭代器可能看起来像这样:

class ThreadSafeIterator<T> implements Iterator<T> {
    private final Queue<T> clients;
    private T currentElement;
    private final Collection<T> source;

    AsynchronousIterator(final Collection<T> collection) {
        clients = new LinkedList<>(collection);
        this.source = collection;
    }

    @Override
    public boolean hasNext() {
        return clients.peek() != null;
    }

    @Override
    public T next() {
        currentElement = clients.poll();
        return currentElement;
    }

    @Override
    public void remove() {
        synchronized(source) {
            source.remove(currentElement);
        }
    }
}

把它作为一个步骤,你可能会使用 Semaphore 用于确保线程安全的类别。但是用一粒盐去除方法。

关键是,通过使用这样的迭代器,没有人,迭代和迭代类(是真正的单词)都不必担心线程安全。


0
2018-02-23 16:19