Java中不同类型的线程安全集


135

在Java中似乎有很多不同的实现和方法来生成线程安全的Set。一些例子包括

1)CopyOnWriteArraySet

2)Collections.synchronizedSet(设置集)

3)ConcurrentSkipListSet

4)Collections.newSetFromMap(new ConcurrentHashMap())

5)以类似于(4)的方式生成的其他集合

这些示例来自Java 6中的并发模式:并发集实现。

有人可以简单解释一下这些示例与其他示例的区别,优点和缺点吗?我在理解和保持Java Std Docs的所有内容方面遇到麻烦。

Answers:


206

1)CopyOnWriteArraySet是一个非常简单的实现-它基本上在数组中具有一个元素列表,并且在更改列表时会复制该数组。此时正在运行的迭代和其他访问将继续使用旧数组,从而避免了读取器和写入器之间进行同步的必要性(尽管写入本身需要进行同步)。contains()此处通常快速设置的操作(尤其是)非常慢,因为将在线性时间内搜索数组。

仅将其用于非常小的集,这些集将经常被读取(重复)并且很少更改。(Swings侦听器集将是一个示例,但它们并不是真正的集,无论如何都应仅在EDT中使用。)

2)Collections.synchronizedSet只需将一个同步块包装在原始集合的每种方法周围。您不应该直接访问原始集。这意味着不能同时执行集合中的两个方法(一个将阻塞直到另一个完成)-这是线程安全的,但是如果多个线程确实在使用该集合,则不会有并发性。如果使用迭代器,则在修改迭代器调用之间的集合时,通常仍需要从外部进行同步以避免ConcurrentModificationExceptions。性能将类似于原始集的性能(但具有一些同步开销,如果同时使用,则会阻塞)。

如果您的并发性很低,并且要确保所有更改对其他线程都是立即可见的,请使用此选项。

3)ConcurrentSkipListSet是并发SortedSet实现,大多数基本操作都在O(log n)中进行。它允许并发添加/删除和读取/迭代,自创建迭代器以来,迭代可能会或可能不会告诉您有关更改的信息。批量操作只是多个单个调用,而不是原子调用-其他线程可能仅观察其中的一些。

显然,只有在元素上具有一定的总顺序时,您才可以使用它。对于不太大的集合(由于O(log n)),这看起来像是高并发情况的理想候选者。

4)对于ConcurrentHashMap(以及从中派生的Set):这里最基本的选项是(平均而言,如果您有一个好的和快速的hashCode())O(1)(但可能会退化为O(n)),就像HashMap / HashSet。写入的并发性有限(表已分区,并且写入访问将在所需的分区上同步),而读取访问与其自身和写入线程完全并发(但可能尚未看到当前正在更改的结果)书面)。自创建以来,迭代器可能会或可能不会看到更改,并且批量操作不是原子的。调整大小很慢(与HashMap / HashSet一样),因此请尝试通过估计创建时所需的大小来避免这种情况(并使用大约1/3的大小,因为在3/4填满时会调整大小)。

当您有大集合,良好(快速)的哈希函数并且在创建地图之前可以估计集合的大小和所需的并发性时,请使用此函数。

5)还有其他并发映射实现可以在这里使用吗?


1
只是在1)中进行视力校正,必须通过同步锁定将数据复制到新阵列中的过程。因此,CopyOnWriteArraySet不能完全避免同步的必要性。
CaptainHastings 2015年

ConcurrentHashMap基于集合的情况下,“因此尝试通过估计创建所需的大小来避免这种情况。” 您提供给地图的大小应比估算值(或已知值)大33%以上,因为该集合会在75%的负载下调整大小。我使用expectedSize + 4 / 3 + 1
Daren 2015年

@Daren我想第一个+应该是*
圣保罗Ebermann

@PaŭloEbermann当然...应该是expectedSize * 4 / 3 + 1
Daren

1
对于Java 8中的ConcurrentMap(或HashMap),如果映射到同一存储桶的条目数达到阈值(我相信是16),则该列表将更改为二进制搜索树(精确的红黑树),并在这种情况下进行查找时间应该是O(lg n),不是O(n)
akhil_mittal '17

20

通过使用和替换每个修改的整个集合,可以将的contains()性能HashSet与并发相关的属性相结合。CopyOnWriteArraySetAtomicReference<Set>

实施草图:

public abstract class CopyOnWriteSet<E> implements Set<E> {

    private final AtomicReference<Set<E>> ref;

    protected CopyOnWriteSet( Collection<? extends E> c ) {
        ref = new AtomicReference<Set<E>>( new HashSet<E>( c ) );
    }

    @Override
    public boolean contains( Object o ) {
        return ref.get().contains( o );
    }

    @Override
    public boolean add( E e ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( current.contains( e ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.add( e );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

    @Override
    public boolean remove( Object o ) {
        while ( true ) {
            Set<E> current = ref.get();
            if ( !current.contains( o ) ) {
                return false;
            }
            Set<E> modified = new HashSet<E>( current );
            modified.remove( o );
            if ( ref.compareAndSet( current, modified ) ) {
                return true;
            }
        }
    }

}

实际上AtomicReference将值标记为volatile。这意味着它可以确保没有线程正在读取过时的数据,并且可以happens-before保证编译器无法对代码进行重新排序。但是,如果仅使用get / set方法,AtomicReference那么我们实际上是在以一种奇特的方式将变量标记为volatile。
akhil_mittal '17

不能充分地回答这个问题,因为(1)除非我错过了什么,否则它将对所有集合类型都有效(2)其他类都没有提供一次原子更新整个集合的方法……这非常有用。
吉利

我尝试使用此逐字记录,但发现它被贴上标签abstract,似乎是为了避免编写几种方法。我开始添加它们,但是遇到了一个障碍iterator()。我不知道如何在不破坏模型的情况下维护该迭代器。似乎我总是必须经历ref,并且每次可能会得到一个不同的基础集,这需要在基础集上获得一个新的迭代器,这对我来说是无用的,因为它将以零项开始。有什么见解吗?
nclark

好的,我想可以保证的是,每个客户都能及时获得固定的快照,因此,如果您需要的话,基础集合的迭代器将可以正常工作。我的用例是允许竞争线程“声明”其中的单个资源,如果它们具有不同版本的集合,则该线程将不起作用。不过,第二...我想我的线程只需要获取一个新的迭代器,然后再试一次,如果CopyOnWriteSet.remove(chosen_item)返回false ...,无论如何它都必须这样做:)
nclark

11

如果Javadocs没有帮助,您可能应该只找一本书或一篇文章来阅读有关数据结构的知识。乍看上去:

  • 每当您对集合进行突变时,CopyOnWriteArraySet都会为基础数组创建一个新副本,因此写入速度很慢,而迭代器则快速且一致。
  • Collections.synchronizedSet()使用老式的同步方法调用来使Set线程安全。这将是一个性能较低的版本。
  • ConcurrentSkipListSet提供具有不一致批处理操作(addAll,removeAll等)和迭代器的高性能写入。
  • Collections.newSetFromMap(new ConcurrentHashMap())具有ConcurrentHashMap的语义,我认为它不一定针对读写进行了优化,但与ConcurrentSkipListSet一样,具有不一致的批处理操作。


1

并发弱引用集

另一个问题是弱引用的线程安全集合。

这样的集合对于在发布订阅场景中跟踪订户很方便。当订户在其他地方超出范围,并因此成为垃圾收集的候选者时,不必为订户优雅地取消订阅而烦恼。弱引用使订阅者可以完成向垃圾收集候选者的过渡。当最终收集垃圾时,将删除集合中的条目。

虽然捆绑类没有直接提供这样的集合,但是您可以通过几次调用来创建一个集合。

首先,我们首先Set利用WeakHashMap类来进行弱引用。这在的类文档中显示Collections.newSetFromMap

Set< YourClassGoesHere > weakHashSet = 
    Collections
    .newSetFromMap(
        new WeakHashMap< YourClassGoesHere , Boolean >()
    )
;

地图的Boolean这里无关紧要,因为地图的构成了我们的Set

在pub-sub之类的方案中,如果订阅者和发布者在不同的线程上运行,则我们需要线程安全(很可能是这种情况)。

通过包装为同步集,使该集成为线程安全的,进一步走了一步。传送至的呼叫Collections.synchronizedSet

this.subscribers =
        Collections.synchronizedSet(
                Collections.newSetFromMap(
                        new WeakHashMap <>()  // Parameterized types `< YourClassGoesHere , Boolean >` are inferred, no need to specify.
                )
        );

现在,我们可以从生成的中添加和删除订阅者Set。执行垃圾收集后,所有“消失的”订户最终都会被自动删除。该执行何时发生取决于您的JVM的垃圾回收器实现,并取决于当前的运行时情况。有关底层结构何时以及如何WeakHashMap清除过期条目的讨论和示例,请参阅以下问题:* WeakHashMap是否正在增长,还是清除了垃圾键?*

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.