1. 概述

在这个教程中,我们将探讨如何创建线程安全的 HashSet 实例,以及与 ConcurrentHashMap 对应的 HashSet 实现。此外,我们还将分析每种方法的优缺点。

2. 使用 ConcurrentHashMap 工厂方法创建线程安全的 HashSet

首先,我们来看看 ConcurrentHashMap 类,它提供了静态的 newKeySet() 方法。这个方法返回一个符合 java.util.Set 接口的实例,支持标准方法如 add(), contains() 等操作。

创建过程很简单:

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

此外,返回的 Set 的性能与普通的 HashSet 相似,因为它们都基于哈希算法实现。而且,由于使用了 ConcurrentHashMap,同步逻辑带来的额外开销也相对较小。

值得注意的是,这个方法从 Java 8 开始可用。

3. 使用 ConcurrentHashMap 实例方法创建线程安全的 HashSet

到目前为止,我们已经了解了 ConcurrentHashMap 的静态方法。接下来,我们将研究 ConcurrentHashMap 提供的实例方法来创建线程安全的 Set 实例。这里有两种方法:newKeySet()newKeySet(defaultValue),它们之间略有不同。

这两种方法创建的 Set 都与原始地图关联。换句话说,每次在源 ConcurrentHashMap 中添加新条目时,Set 也会接收到该值。现在让我们看看这两种方法的区别。

3.1. newKeySet() 方法

如上所述,newKeySet() 返回包含源地图所有键的 Set。与 newKeySet(defaultValue) 的主要区别在于,前者不支持向 Set 添加新元素。如果尝试调用 add()addAll() 方法,会抛出 UnsupportedOperationException

尽管 remove(object)clear() 操作正常工作,但我们需要注意,对 Set 的任何修改都会反映在原始地图中:

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}

3.2. newKeySet(defaultValue) 方法

另一种从地图中的键创建 Set 的方法是 newKeySet(defaultValue)。相比之前的方法,它返回的 Set 实例支持通过调用 add()addAll()Set 上添加新元素。

传递的默认值参数将用于通过 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]

4. 使用 Collections 工具类创建线程安全的 HashSet

我们可以利用 java.util.Collections 类提供的 synchronizedSet() 方法创建线程安全的 HashSet 实例:

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

在使用这种方法之前,请注意它的效率不如前面讨论的那些方法。synchronizedSet() 只是将 Set 实例包装在一个同步装饰器中,而 ConcurrentHashMap 实现了底层的并发机制。

5. 使用 CopyOnWriteArraySet 创建线程安全的 Set

最后一个创建线程安全 Set 实现的方法是 CopyOnWriteArraySet。创建此类实例非常简单:

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

尽管这个类看起来很有吸引力,但我们需要考虑其严重的性能问题。CopyOnWriteArraySet 在内部使用数组而不是哈希表存储数据。这意味着像 contains()remove() 这样的操作具有 O(n) 的复杂度,而使用基于 ConcurrentHashMapSet,复杂度为 O(1)。

建议在 Set 的大小通常较小且读取操作占多数的情况下使用这种实现。

6. 总结

在这篇文章中,我们探讨了创建线程安全 Set 实例的不同方法,并强调了它们之间的差异。首先推荐使用 ConcurrentHashMap.newKeySet() 静态方法创建线程安全的 HashSet。然后,我们比较了 ConcurrentHashMap 静态方法和 newKeySet()newKeySet(defaultValue) 方法在 ConcurrentHashMap 实例上的差异。

最后,我们讨论了 Collections.synchronizedSet()CopyOnWriteArraySet,以及它们的性能影响。

如往常一样,完整的源代码可以在 GitHub 查看。