线程安全的集合遍历

19
我们都知道使用 Collections.synchronizedXXX(例如synchronizedSet())时,我们会得到底层集合的同步“视图”。
然而,这些包装方法的文档声明,在使用迭代器迭代集合时,我们必须明确地对集合进行同步
您要选择哪个选项来解决这个问题?
我只能看到以下方法:
  1. 按照文档所述执行:在集合上进行同步
  2. 在调用iterator()之前克隆集合
  3. 使用具有线程安全迭代器的集合(我只知道CopyOnWriteArrayList/Set)
作为额外的问题:当使用同步视图时,使用foreach/Iterable是线程安全的吗?

或者通过使用ExecutorService或类似的东西来避免共享迭代器(使用迭代器将新的Callable/Runnable添加到ExecutorService中)。其他一些集合类型宣传自己是“线程安全”的,我想知道它们的迭代器是否也是如此:stackoverflow.com/a/3362063/32453(我有点怀疑)。 - rogerdpack
8个回答

27

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

至于哪种方法最适合-这真的取决于你的背景:

  • 写入非常不频繁吗? 如果是这样,CopyOnWriteArrayList 可能是最合适的。
  • 集合大小相对较小,迭代速度快吗? (即在循环中没有进行太多的工作)如果是这样,同步可能是可以接受的 - 特别是如果不经常发生这种情况(即您不会对集合有太多争用)。
  • 如果要执行大量的工作并且不想阻塞其他线程同时工作,那么克隆集合的开销可能是可以接受的。

3
foreach 循环是否保证始终使用 iterator(),还是这取决于具体的实现? - MRalwasser
4
这段话的意思是:除了数组使用其索引值之外,规范保证了其他部分的正确性。可以参考JLS的第14.14.2节进行查看:http://java.sun.com/docs/books/jls/third_edition/html/statements.html#14.14.2 - Jon Skeet

6

根据您的访问模式而定。如果您的并发较低且写入频繁,则1具有最佳性能。如果您的并发较高且写入不频繁,则3具有最佳性能。在几乎所有情况下,选项2的性能都很差。

foreach 调用 iterator(),因此完全适用相同的原则。


4

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

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

}

1

我可能完全不了解你的需求,但如果你还没有意识到它们,可以考虑使用Google Collections并牢记“偏爱不可变性”。


1

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


1

你的三个选项都可以运作。选择合适的选项取决于你的具体情况。

如果你需要一个列表实现,而且不介意每次写入时复制底层存储,那么CopyOnWriteArrayList就可以胜任。只要你没有非常大的集合,这样的性能表现还是相当不错的。

如果你需要MapSet接口,那么ConcurrentHashMap或者使用Collections.newSetFromMap创建的"ConcurrentHashSet"就可以胜任。显然通过这种方式不能进行随机访问。这两种的优点之一在于,它们可以很好地处理大型数据集-当它们发生变化时,它们只会复制底层数据存储的一小部分。


0

这个问题有点老了(抱歉,我有点晚了..),但我仍然想要添加我的答案。

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

假设你想使用iterator进行迭代,你不必在调用.iterator()之前复制Collection,并且有点否定了迭代器模式的思想,而是可以编写一个“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

这取决于需要实现克隆/复制/toArray()等操作的结果,new ArrayList(..)等方法可以获得快照并且不会锁定集合。 使用synchronized(collection)和迭代确保在迭代结束时没有修改,即有效地锁定它。

顺便提一下:(通常情况下,toArray()是首选,除非内部需要创建临时ArrayList)。此外,请注意,除了toArray()之外的任何操作都应该包装在synchronized(collection)中,前提是使用Collections.synchronizedXXX。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接