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

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

1) CopyOnWriteArraySet

2) SynizedSet (Set Set)

3) ConcurrentSkipListSet

4) NewSetFromMap (new ConcurrentHashMap ())

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

这些例子来自 併发型模式: Java6中的并发集实现

有没有人能简单地解释一下这些例子和其他例子的区别、优点和缺点?我无法理解并理解 Java 标准文档中的所有内容。

107146 次浏览

如果 Javadocs 没有帮助,您可能应该找一本关于数据结构的书或文章来阅读。一眼望去:

  • 每次更改集合时,CopyOnWriteArraySet 都会创建一个基础数组的新副本,因此写入速度较慢,迭代器速度快且一致。
  • SynizedSet ()使用老式的同步方法调用来创建 Set threadsafe。
  • ConcurrentSkipListSet 提供了具有不一致批处理操作(addAll、 RemoveAll 等)和迭代器的性能写。
  • NewSetFromMap (new ConcurrentHashMap ())具有 ConcurrentHashMap 的语义,我认为它不一定针对读或写进行了优化,但是像 ConcurrentSkipListSet 一样,具有不一致的批处理操作。
  1. CopyOnWriteArraySet是一个非常简单的实现-它基本上在一个数组中有一个元素列表,当更改该列表时,它将复制该数组。此时正在运行的迭代和其他访问继续使用旧的数组,避免了读取器和写入器之间同步的必要性(尽管写入本身需要同步)。通常快速设置操作(特别是 contains())在这里相当慢,因为数组将在线性时间内搜索。

    仅对经常读(迭代)且很少更改的真正小的集合使用此方法。(Swing 的侦听器集就是一个例子,但是它们并不是真正的集合,而且应该只在 EDT 中使用。)

  2. Collections.synchronizedSet将简单地在原始集合的每个方法周围包装一个 sync-block。不应直接访问原始集。这意味着这个集合中没有两个方法可以并发执行(一个方法会被阻塞,直到另一个方法完成)——这是线程安全的,但是如果有多个线程在使用这个集合,就不会有并发性。如果使用迭代器,通常仍然需要在外部进行同步,以避免在迭代器调用之间修改集时出现 ConcurrentModficationException。性能将类似于原始集的性能(但是有一些同步开销,如果同时使用则会阻塞)。

    如果您的并发性较低,并且希望确保所有更改对其他线程都是可见的,则可以使用此选项。

  3. ConcurrentSkipListSet是并发的 SortedSet实现,其中大多数基本操作是 O (logn)。它允许并发添加/删除和读取/迭代,其中迭代可能会或可能不会告诉自创建迭代器以来的更改。批量操作只是多个单个调用,并不是自动完成的——其他线程可能只观察其中的一部分。

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

  4. 对于 ConcurrentHashMap(以及从它派生出来的 Set) : 这里大多数基本选项是(平均来说,如果你有一个好的和快速的 hashCode())在 O (1)中(但是当许多键有相同的哈希代码时可能退化为 O (n)) ,就像 HashMap/HashSet。写入的并发性是有限的(表是分区的,并且写访问将在所需的分区上同步) ,而读访问与它自己和写线程是完全并发的(但是可能还看不到当前正在写入的更改的结果)。迭代器自创建以来可能看到更改,也可能看不到更改,而且批量操作不是原子操作。 调整大小很慢(对于 HashMap/HashSet) ,因此您应该通过在创建时估计所需的大小来避免这种情况(并且使用大约1/3的大小,因为它在3/4满时会调整大小)。

    当您有大的集合、一个好的(和快速的)散列函数并且可以在创建映射之前估计集合的大小和需要的并发性时使用它。

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

通过使用 AtomicReference<Set>并在每次修改时替换整个集合,可以将 HashSetcontains()性能与 CopyOnWriteArraySet的并发相关属性结合起来。

实施大纲:

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;
}
}
}


}

弱引用的并发集

另一个转折是线程安全的 参考文献不足集。

这样的集合对于在 酒吧三明治场景中跟踪订阅者非常方便。当订阅者在其他地方超出了作用域范围,并因此朝着成为垃圾收集候选者的方向发展时,订阅者不必为优雅地取消订阅而烦恼。弱引用允许订阅者完成转换,成为垃圾收集的候选者。当最终收集到垃圾时,将删除集合中的条目。

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

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

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

地图的 价值Boolean,在这里是不相关的,因为地图的 钥匙构成了我们的 Set

在发布-订阅这样的场景中,如果订阅者和发布者在不同的线程上操作(很可能是这种情况) ,那么我们需要线程安全。

更进一步,将该集合包装为同步集合,使其成为线程安全的。输入到对 Collections.synchronizedSet的调用中。

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

现在,我们可以从生成的 Set中添加和删除订阅者。在执行垃圾收集之后,任何“消失”的订阅者最终都将被自动删除。执行的时间取决于 JVM 的垃圾收集器实现,并且取决于当前的运行时情况。有关底层 WeakHashMap何时以及如何清除过期条目的讨论和示例,请参见下面的问题,< a href = “ https://stackoverflow./q/52789834/642706”> * WeakHashMap 是在不断增长,还是清除了垃圾键? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *