ChatGPT解决这个技术问题 Extra ChatGPT

为什么没有针对 ConcurrentHashMap 的 ConcurrentHashSet

HashSet 基于 HashMap。

如果我们查看 HashSet<E> 实现,所有内容都在 HashMap<E,Object> 下进行管理。

<E> 用作 HashMap 的键。

而且我们知道 HashMap 不是线程安全的。这就是我们在 Java 中使用 ConcurrentHashMap 的原因。

基于此,我很困惑为什么我们没有应该基于 ConcurrentHashMap 的 ConcurrentHashSet?

还有什么我想念的吗?我需要在多线程环境中使用 Set

另外,如果我想创建自己的 ConcurrentHashSet,是否可以通过将 HashMap 替换为 ConcurrentHashMap 并将其余部分保持原样来实现它?

在查看 API 之后,如果我猜测我会说它似乎归结为 2 个因素,(1)避免在 Java API 中为所需的每一点功能创建一个类(2)为更常用的对象。我个人更喜欢 LinkedHashMap 和 LinkedHashSet 因为它们保证顺序与插入顺序相同,使用集合的唯一原因是避免重复,通常我仍然想保持插入顺序。
@Ali,我个人更喜欢 LinkedHashMap 和 LinkedHashSet 你会走得很远:)
一个有点老的问题,但由于它是谷歌的第一个结果,知道 ConcurrentSkipListSet 已经实现了 ConcurrentHashMap 可能很有用。请参阅docs.oracle.com/javase/7/docs/api/java/util/concurrent/…
我从 Java 源代码 ConcurrentSkipListSet 中看到的内容是基于 ConcurrentSkipListMap 构建的,它实现了 ConcurrentNavigableMapConcurrentMap

R
Ray Toal

ConcurrentHashSet 没有内置类型,因为您始终可以从地图中派生一个集合。由于有许多类型的地图,您可以使用一种方法从给定的地图(或地图类)生成一个集合。

在 Java 8 之前,您可以使用 Collections.newSetFromMap(map) 生成由并发哈希映射支持的并发哈希集

在 Java 8(@Matt 指出)中,您可以通过 ConcurrentHashMap.newKeySet() 获取并发散列集 view。这比旧的 newSetFromMap 要简单一些,旧的 newSetFromMap 需要您传入一个空的地图对象。但它特定于 ConcurrentHashMap

无论如何,Java 设计者可以在每次创建新的地图界面时创建一个新的集合界面,但是当第三方创建他们自己的地图时,这种模式将无法实施。最好有派生新集合的静态方法;即使您创建自己的地图实现,这种方法也始终有效。


我是否正确地说,如果您以这种方式从 ConcurrentHashMap 创建集合,您将失去从 ConcurrentHashMap 获得的好处?
没有任何好处可以失去。 newSetFromMap 的实现从 docjar.com/html/api/java/util/Collections.java.html 的第 3841 行开始。这只是一个包装......
@Andrew,我认为使用“ConcurrentSet”背后的动机不是源于API,而是源于实现——线程安全但没有通用锁——例如多个并发读取。
ConcurrentSkipList 有很多(大小)开销,并且查找速度较慢。
使用这种方法时要小心,因为有些方法没有正确实现。只需点击链接:Collections.newSetFromMap 创建一个 SetFromMap。例如,SetFromMap.removeAll 方法委托给从 ConcurrentHashMap$CollectionView.removeAll 继承的 KeySetView.removeAll。这种方法在批量去除元素方面效率很低。想象一下 removeAll(Collections.emptySet()) 遍历 Map 中的所有元素而不做任何事情。在大多数情况下,正确实现 ConcurrentHashSet 会更好。
P
Paresh Mayani
Set<String> mySet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

正如 Ray Toal 在他的回答中指出的那样,2021 年的首选方法肯定是 ConcurrentHashMap.newKeySet() (它在幕后做类似的事情),因为它更简洁。
这是线程安全的吧?
k
kichik

使用 Guava 15 您还可以简单地使用:

Set s = Sets.newConcurrentHashSet();

这总是一场噩梦。如果您有一个集合或映射不指示某些东西是否是线程安全的,那么您会发现在维护过程中会发生各种危险和灾难。我总是想要一种指示集合(或不)的线程安全的类型。
方法描述的字面意思是“创建一个由哈希映射支持的线程安全集”
正如我所说,缺少一个 ConcurrentSet 。 ConcurrentHashMap 带有一个 ConcurrentMap 接口来表明这一点。这也是我总是添加这个 ConcurrentSet 接口的原因。
C
Community

就像 Ray Toal 提到的那样简单:

Set<String> myConcurrentSet = ConcurrentHashMap.newKeySet();

这似乎需要 Java 8。从实现来看,这似乎也只是 ConcurrentHashMap 的包装。
M
Mike Pone

看起来 Java 通过其 ConcurrentSkipListSet 提供了并发 Set 实现。 SkipList Set 只是一种特殊的集合实现。它仍然实现了 Serializable、Cloneable、Iterable、Collection、NavigableSet、Set、SortedSet 接口。如果您只需要 Set 界面,这可能对您有用。


请注意,ConcurrentSkipListSet 的元素应为 Comparable
如果您需要从并发的 Set 扩展,这是唯一可行的解决方案。
ConcurrentSkipListMap 增加了将树作为基本数据结构而不是使用 HashTable 的不必要的性能损失,即使您不需要排序/导航功能也是如此。
除非您需要 SortedSet,否则不要使用 ConcurrentSkipListSet。像添加或删除这样的常用操作对于 HashSet 应该是 O(1),而对于 SortedSet 应该是 O(log(n))。
N
Nirro

正如 this 所指出的,获得并发 HashSet 的最佳方法是通过 Collections.synchronizedSet()

Set s = Collections.synchronizedSet(new HashSet(...));

这对我有用,我还没有看到有人真正指出它。

EDIT 正如 Eugene 指出的那样,这比当前已批准的解决方案效率低,因为它只是将您的集合包装到同步装饰器中,而 ConcurrentHashMap 实际上实现了低级并发,它可以支持您的设置一样好。感谢 Stepanenkov 先生明确表示。

http://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#synchronizedSet-java.util.Set-


synchronizedSet 方法只是在 Collection 下创建 decorator 以包装可以通过同步整个集合来实现线程安全的方法。但是 ConcurrentHashMap 是使用 non-blocking algorithms 和“低级”同步实现的,没有整个集合的任何锁。因此,出于性能原因,来自 Collections.synchronized... 的包装器在多线程环境中更差。
B
Bozho

您可以使用番石榴的 Sets.newSetFromMap(map) 来获得一个。 Java 6 在 java.util.Collections 中也有该方法


它在 java.utll.Collections 中可用,而且 CHM 集通常是一件坏事。
是的,我注意到它是在 Java 6 中添加的,所以将它添加到答案中
主要是如果它是ThreadSafe,我真的很怀疑。
@Talha,它是线程安全的,但是线程安全本身没有任何意义
有时它意味着一切。除非它是通常以最小化并发映射需求的方式实现的算法的一部分,否则它很少会成为性能问题。
A
Ankush soni
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


public class ConcurrentHashSet<E> extends AbstractSet<E> implements Set<E>{
   private final ConcurrentMap<E, Object> theMap;

   private static final Object dummy = new Object();

   public ConcurrentHashSet(){
      theMap = new ConcurrentHashMap<E, Object>();
   }

   @Override
   public int size() {
      return theMap.size();
   }

   @Override
   public Iterator<E> iterator(){
      return theMap.keySet().iterator();
   }

   @Override
   public boolean isEmpty(){
      return theMap.isEmpty();
   }

   @Override
   public boolean add(final E o){
      return theMap.put(o, ConcurrentHashSet.dummy) == null;
   }

   @Override
   public boolean contains(final Object o){
      return theMap.containsKey(o);
   }

   @Override
   public void clear(){
      theMap.clear();
   }

   @Override
   public boolean remove(final Object o){
      return theMap.remove(o) == ConcurrentHashSet.dummy;
   }

   public boolean addIfAbsent(final E o){
      Object obj = theMap.putIfAbsent(o, ConcurrentHashSet.dummy);
      return obj == null;
   }
}

我喜欢使用 Boolean.TRUE 而不是虚拟对象的想法。它更优雅一点。也可以使用 NULL,因为即使映射为 null,它也可以在键集中使用。
@MartinKersten 仅供参考,ConcurrentHashMap 不允许空值
S
Shendor

为什么不使用:来自 java.util.concurrent 的 CopyOnWriteArraySet?


因为 CopyOnWriteArraySet 在任何状态突变时复制整个集合,由于性能影响,这并不总是需要的。它旨在仅在特殊情况下工作。
此外,CopyOnWriteArraySet.contains() 的运行时间为 O(n)(必须检查任何条目),而 HashSet/HashMap 有 O(1)
因为这只适用于一种方式:如果您正在读取由另一个线程拥有/管理的数据。如果您需要将数据传递给该线程,则必须另外将其原子引用更新为更新的数组-但这没有什么意义