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) 的复杂度,而使用基于 ConcurrentHashMap
的 Set
,复杂度为 O(1)。
建议在 Set
的大小通常较小且读取操作占多数的情况下使用这种实现。
6. 总结
在这篇文章中,我们探讨了创建线程安全 Set
实例的不同方法,并强调了它们之间的差异。首先推荐使用 ConcurrentHashMap.newKeySet()
静态方法创建线程安全的 HashSet
。然后,我们比较了 ConcurrentHashMap
静态方法和 newKeySet()
、newKeySet(defaultValue)
方法在 ConcurrentHashMap
实例上的差异。
最后,我们讨论了 Collections.synchronizedSet()
和 CopyOnWriteArraySet
,以及它们的性能影响。
如往常一样,完整的源代码可以在 GitHub 查看。