在Java中似乎有很多不同的实现和方法来生成线程安全的Set。一些例子包括
2)Collections.synchronizedSet(设置集)
4)Collections.newSetFromMap(new ConcurrentHashMap())
5)以类似于(4)的方式生成的其他集合
这些示例来自Java 6中的并发模式:并发集实现。
有人可以简单解释一下这些示例与其他示例的区别,优点和缺点吗?我在理解和保持Java Std Docs的所有内容方面遇到麻烦。
在Java中似乎有很多不同的实现和方法来生成线程安全的Set。一些例子包括
2)Collections.synchronizedSet(设置集)
4)Collections.newSetFromMap(new ConcurrentHashMap())
5)以类似于(4)的方式生成的其他集合
这些示例来自Java 6中的并发模式:并发集实现。
有人可以简单解释一下这些示例与其他示例的区别,优点和缺点吗?我在理解和保持Java Std Docs的所有内容方面遇到麻烦。
Answers:
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)还有其他并发映射实现可以在这里使用吗?
ConcurrentHashMap
基于集合的情况下,“因此尝试通过估计创建所需的大小来避免这种情况。” 您提供给地图的大小应比估算值(或已知值)大33%以上,因为该集合会在75%的负载下调整大小。我使用expectedSize + 4 / 3 + 1
+
应该是*
?
expectedSize * 4 / 3 + 1
ConcurrentMap
(或HashMap
),如果映射到同一存储桶的条目数达到阈值(我相信是16),则该列表将更改为二进制搜索树(精确的红黑树),并在这种情况下进行查找时间应该是O(lg n)
,不是O(n)
。
通过使用和替换每个修改的整个集合,可以将的contains()
性能HashSet
与并发相关的属性相结合。CopyOnWriteArraySet
AtomicReference<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。
abstract
,似乎是为了避免编写几种方法。我开始添加它们,但是遇到了一个障碍iterator()
。我不知道如何在不破坏模型的情况下维护该迭代器。似乎我总是必须经历ref
,并且每次可能会得到一个不同的基础集,这需要在基础集上获得一个新的迭代器,这对我来说是无用的,因为它将以零项开始。有什么见解吗?
如果Javadocs没有帮助,您可能应该只找一本书或一篇文章来阅读有关数据结构的知识。乍看上去:
另一个问题是弱引用的线程安全集合。
这样的集合对于在发布订阅场景中跟踪订户很方便。当订户在其他地方超出范围,并因此成为垃圾收集的候选者时,不必为订户优雅地取消订阅而烦恼。弱引用使订阅者可以完成向垃圾收集候选者的过渡。当最终收集垃圾时,将删除集合中的条目。
虽然捆绑类没有直接提供这样的集合,但是您可以通过几次调用来创建一个集合。
首先,我们首先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是否正在增长,还是清除了垃圾键?*。