Java中实现线程安全HashSet的几种方法 | baeldung


在本教程中,我们将了解创建线程安全HashSet实例的可能性以及HashSet的ConcurrentHashMap的等价物。此外,我们将研究每种方法的优缺点。
 
使用ConcurrentHashMap工厂方法的线程安全HashSet
首先,我们将查看公开静态newKeySet()方法的ConcurrentHashMap类。基本上,此方法返回一个尊重java.util.Set接口的实例,并允许使用标准方法,如add()、contains()等。
这可以简单地创建为:

Set<Integer> threadSafeUniqueNumbers = ConcurrentHashMap.newKeySet();
threadSafeUniqueNumbers.add(23);
threadSafeUniqueNumbers.add(45);

此外,返回的Set的性能类似于Has hSet,因为两者都是使用基于哈希的算法实现的。此外,同步逻辑带来的额外开销也很小,因为实现使用了ConcurrentHashMap。
最后,缺点是该方法仅从 Java 8 开始存在。
 
使用ConcurrentHashMap实例方法的线程安全HashSet
至此,我们已经了解了ConcurrentHashMap 的静态方法。接下来,我们将处理可用于ConcurrentHashMap的实例方法来创建线程安全的Set实例。有两种方法可用,newKeySet()和newKeySet(defaultValue),它们彼此略有不同。
这两种方法都创建了一个与原始map链接的Set。换句话说,每次我们向原始ConcurrentHashMap 添加一个新条目时, Set都会接收该值。此外,让我们看看这两种方法之间的区别。

  • newKeySet ()方法

如上所述,newKeySet()公开了一个包含原始映射的所有键的Set 。此方法与newKeySet(defaultValue)的主要区别在于当前方法不支持向Set添加新元素。因此,如果我们尝试调用add()或addAll() 之类的方法,我们将得到 UnsupportedOperationException。
尽管remove(object)或clear()之类的操作按预期工作,但我们需要注意Set上的任何更改都将反映在原始映射map中:
ConcurrentHashMap<Integer,String> numbersMap = new ConcurrentHashMap<>();
Set<Integer> numbersSet = numbersMap.keySet();

numbersMap.put(1, "One");
numbersMap.put(2,
"Two");
numbersMap.put(3,
"Three");

System.out.println(
"Map before remove: "+ numbersMap);
System.out.println(
"Set before remove: "+ numbersSet);

numbersSet.remove(2);

System.out.println(
"Set after remove: "+ numbersSet);
System.out.println(
"Map after remove: "+ numbersMap);

输出:

Map before remove: {1=One, 2=Two, 3=Three}
Set before remove: [1, 2, 3]

Set after remove: [1, 3]
Map after remove: {1=One, 3=Three}

 
  • newKeySet (defaultValue)方法

让我们看看另一种使用地图中的键创建Set的方法。与上面提到的相比,newKeySet(defaultValue)返回一个Set实例,该实例支持通过调用set 上的add()或addAll()来添加新元素。
进一步查看作为参数传递的默认值,这被用作地图中添加的每个新条目的值add()或addAll()方法。以下示例显示了它是如何工作的:
ConcurrentHashMap<Integer,String> numbersMap = new ConcurrentHashMap<>();
Set<Integer> numbersSet = numbersMap.keySet("SET-ENTRY");

numbersMap.put(1,
"One");
numbersMap.put(2,
"Two");
numbersMap.put(3,
"Three");

System.out.println(
"Map before add: "+ numbersMap);
System.out.println(
"Set before add: "+ numbersSet);

numbersSet.addAll(asList(4,5));

System.out.println(
"Map after add: "+ numbersMap);
System.out.println(
"Set after add: "+ numbersSet);

下面是上面代码的输出:

Map before add: {1=One, 2=Two, 3=Three}
Set before add: [1, 2, 3]
Map after add: {1=One, 2=Two, 3=Three, 4=SET-ENTRY, 5=SET-ENTRY}
Set after add: [1, 2, 3, 4, 5]

 
使用集合实用程序类的线程安全HashSet
让我们使用java.util.Collections 中可用的synchronizedSet()方法来创建一个线程安全的HashSet实例:

Set<Integer> syncNumbers = Collections.synchronizedSet(new HashSet<>());
syncNumbers.add(1);

在使用这种方法之前,我们需要意识到它的效率不如上面讨论的那些。与实现低级并发机制的ConcurrentHashMap相比, synchronizedSet()基本上 只是将Set实例包装到同步装饰器中。
 
使用CopyOnWriteArraySet 的线程安全集
创建线程安全Set实现的最后一种方法是CopyOnWriteArraySet。创建这个Set的实例很简单:

Set<Integer> copyOnArraySet = new CopyOnWriteArraySet<>();
copyOnArraySet.add(1);

尽管使用这个类看起来很有吸引力,但我们需要考虑一些严重的性能缺陷。在幕后,CopyOnWriteArraySet使用Array 而不是HashMap来存储数据。这意味着像contains()或remove()这样的操作有 O(n) 的复杂度,而当使用由ConcurrentHashMap 支持的 Set 时,复杂度是 O(1)。

建议在Set大小通常保持较小且只读操作占多数时使用此实现。
 
结论
在本文中,我们看到了创建线程安全Set实例的不同可能性,并强调了它们之间的区别。首先我们看到了ConcurrentHashMap.newKeySet() 静态方法。当需要线程安全的HashSet时,这应该是首选。之后我们查看了ConcurrentHashMap静态方法和  用于ConcurrentHashMap 实例的newKeySet()、newKeySet(defaultValue)之间的区别。
最后我们还讨论了集合。synchronizedSet()和CopyOnWriteArraySet 存在性能缺陷。
像往常一样,完整的源代码可以在 GitHub 上找到