为什么没有ConcurrentHashSet,而有ConcurrentHashMap?

687

HashSet基于HashMap。

如果我们看一下HashSet<E>的实现,所有的东西都被管理在HashMap<E,Object>中。

<E>被用作HashMap的键。

而我们知道HashMap不是线程安全的。这就是为什么Java有ConcurrentHashMap

基于此,我很困惑:为什么我们没有一个基于ConcurrentHashMap的ConcurrentHashSet呢?

还有其他我需要注意的事情吗?我需要在多线程环境中使用Set

另外,如果我想创建自己的ConcurrentHashSet,我可以只将HashMap替换为ConcurrentHashMap,其余内容保持不变吗?


2
在查看了API之后,如果我要猜的话,我会说它似乎归结为两个因素:(1)避免为每一个小功能创建一个Java API类 (2) 为更频繁使用的对象提供方便的类。我个人更喜欢LinkedHashMap和LinkedHashSet,因为它们保证顺序与插入顺序相同,使用集合的唯一原因是避免重复,但通常我仍然希望保持插入顺序。 - Ali
1
@Ali,我个人更喜欢使用LinkedHashMap和LinkedHashSet,在编程方面你会取得很大的进展的 :) - bestsss
9
虽然这个问题有点老,但由于它是谷歌搜索的第一个结果,所以了解到ConcurrentSkipListSet已经实现了ConcurrentHashMap可能很有用。请参见http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentSkipListSet.html。 - Igor Rodriguez
1
我从Java源代码ConcurrentSkipListSet中看到,它是建立在ConcurrentSkipListMap之上的,而ConcurrentSkipListMap实现了ConcurrentNavigableMapConcurrentMap - Talha Ahmed Khan
非常好的观点。也许Java的人认为总是把事情联系起来是件好事......所以他们热烈推崇这个想法! - Victor
显示剩余2条评论
10个回答

733

因为您可以从一个map中派生出set,所以没有ConcurrentHashSet的内置类型。由于有许多类型的map,因此您使用一种方法从给定的map(或map类)生成set。

在Java 8之前,通过使用Collections.newSetFromMap(map)产生支持并发哈希映射的并发哈希集。

在Java 8中(由@Matt指出),您可以通过ConcurrentHashMap.newKeySet()获得并发哈希集视图。newSetFromMap比旧的方式简单一些,因为它需要传入一个空的map对象。但它只适用于ConcurrentHashMap

无论如何,Java设计者都可以在创建新的map接口时每次创建一个新的set接口,但是这种模式在第三方创建自己的映射时无法强制执行。最好具有导出新集合的静态方法;即使您创建自己的映射实现,该方法始终有效。


5
如果你使用这种方式从ConcurrentHashMap创建集合,那么你失去了从ConcurrentHashMap获得的好处,这种说法正确吗? - Pacerier
21
没有任何利益会丧失。newSetFromMap的实现可以在http://www.docjar.com/html/api/java/util/Collections.java.html的第3841行开始找到。它只是一个包装器... - Ray Toal
6
我认为使用“ConcurrentSet”的动机在于其实现,而不是API——即实现线程安全但没有通用锁定——例如允许多个并发读取。请注意不要改变原意,使内容更通俗易懂。 - Ustaman Sangat
5
ConcurrentSkipList 存在许多额外开销(size overhead),并且查找速度较慢。 - eckes
8
使用这种方法时要小心,因为某些方法的实现不正确。只需按照链接操作:“Collections.newSetFromMap”创建一个“SetFromMap”。例如,“SetFromMap.removeAll”方法委托给继承自“ConcurrentHashMap$CollectionView.removeAll”的“KeySetView.removeAll”方法。这种方法在批量移除元素方面效率非常低下。想象一下,“removeAll(Collections.emptySet())”会遍历“Map”中所有的元素而不执行任何操作。使用正确实现的“ConcurrentHashSet”在大多数情况下会更好。 - benez
显示剩余14条评论

126
在Java 8之前:
Set<String> mySet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

Java 8或更高版本:
ConcurrentHashMap.KeySetView<String, Boolean> mySet = ConcurrentHashMap.newKeySet();

5
正如Ray Toal在他的回答中指出的那样,2021年这样做的首选方法肯定是ConcurrentHashMap.newKeySet()(它在后台执行类似的操作),因为它更加简洁。 - Alexander Terp
这是线程安全的吗? - Ashika Umanga Umagiliya
是的,它是线程安全的。 - undefined

102

使用Guava 15, 您还可以简单地使用以下方法:

Set s = Sets.newConcurrentHashSet();

16
这总是一个噩梦。如果你有一个集合或映射,没有指示某个东西是否线程安全,那么在维护过程中会发生各种危险和灾难。我总是希望有一种类型能够表示集合的线程安全性(或者不安全)。 - Martin Kersten
13
这个方法的字面意思是“创建一个由哈希映射支持的线程安全集合”。 - kichik
18
如我所说,缺少一个ConcurrentSet<E>。ConcurrentHashMap随之而来的是一个ConcurrentMap接口来指示这一点。这正是我总是添加这个ConcurrentSet接口的原因。 - Martin Kersten

84

就像Ray Toal所提到的那样,这很容易:

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

1
这似乎需要Java 8。从实现来看,这似乎只是ConcurrentHashMap的包装器。 - Mygod

23

看起来Java提供了一个并发Set实现,使用ConcurrentSkipListSetSkipList Set只是一种特殊类型的Set实现。它仍然实现了Serializable、Cloneable、Iterable、Collection、NavigableSet、Set、SortedSet接口。如果你只需要Set接口,那么这可能适合你。


12
请注意,ConcurrentSkipListSet的元素应该是Comparable - user454322
如果您需要扩展一个并发的Set,这是唯一可行的解决方案。 - ndm13
ConcurrentSkipListMap 在没有排序 / 导航需求时,使用树作为基本数据结构会增加不必要的性能损失,而不是使用 HashTable。 - Ajeet Ganga
6
如果你只需要一个SortedSet,那么使用ConcurrentSkipListSet;否则不要使用它。对于一个HashSet来说,像添加或删除这样的常规操作应该是O(1),但对于一个SortedSet来说,则应该是O(log(n))。 - benez

20

正如这篇文章所指出的,获得一个支持并发操作的HashSet最好的方法是使用Collections.synchronizedSet()

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

这对我很有用,而且我没有看到任何人真正指向它。

编辑 正如Eugene指出的那样,这比当前批准的解决方案效率低,因为它只是将您的集合包装成一个同步装饰器,而ConcurrentHashMap实际上实现了低级并发性,并且可以很好地支持您的Set。因此感谢Stepanenkov先生澄清这一点。

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


26
synchronizedSet方法只是在Collection下创建一个装饰器来封装那些可以通过同步整个集合来保证线程安全的方法。但是ConcurrentHashMap使用非阻塞算法和“低级”同步,而没有任何锁定整个集合。因此,在多线程环境中,使用Collections.synchronized创建的包装器出于性能原因不如ConcurrentHashMap - Eugene Stepanenkov

14

它可以在java.util.Collections中找到,而且通常情况下,使用CHM的集合是不好的。 - bestsss
是的,我注意到它在Java 6中被添加了,所以把它添加到了答案中。 - Bozho
最重要的是它是否是线程安全的,我真的很怀疑。 - Talha Ahmed Khan
@Talha,它是线程安全的,但仅有线程安全并不意味着什么。 - bestsss
有时它意味着一切。除非它是算法的一部分,通常实现方式会尽量减少并发映射的需求,否则很少出现性能问题。 - Martin Kersten
确实,我曾经在使用Sets.newSetFromMap(map)时遇到了可见性问题,自那以后就转而使用ConcurrentHashMap.newKeySet()。 - Sumit Jain

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

3
我喜欢使用Boolean.TRUE而不是虚拟对象的想法。这样会更加优雅一些。同时,使用NULL也是可以的,因为即使映射到null,它仍然可以在键集中使用。 - Martin Kersten
3
了解,ConcurrentHashMap 不允许空值。 - Lauri Lehtinen

2

为什么不使用Java的java.util.concurrent.CopyOnWriteArraySet?


15
由于CopyOnWriteArraySet在任何状态变化时都会复制整个集合,这并不总是想要的,因为会对性能产生影响。它只被设计用于特殊情况下的工作。 - boneash
另外,CopyOnWriteArraySet.contains() 的运行时间为 O(n)(必须检查每个条目),而 HashSet/HashMap 则为 O(1) - Robert
因为这只能单向工作:如果您正在读取另一个线程拥有/管理的数据。如果您需要将数据传递给该线程,则还必须更新其原子引用以更新数组 - 但这没有多大意义。 - mantrid

0

1
你的回答可以通过添加更多的支持性信息来改进。请[编辑]以添加进一步的细节,例如引用或文档,以便其他人可以确认你的回答是否正确。你可以在帮助中心找到关于如何编写好回答的更多信息。 - jv-k

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