Java Concurrent HashSet Equivalent to ConcurrentHashMap – Java并发HashSet等同于ConcurrentHashMap

最后修改: 2022年 1月 23日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll see what are the possibilities to create thread-safe HashSet instances and what is the equivalent of the ConcurrentHashMap for HashSet. Furthermore, we’ll look at the benefits and drawbacks of each approach.

在本教程中,我们将看到创建线程安全的HashSet实例的可能性,以及ConcurrentHashMap对于HashSet的等价物。此外,我们还将看看每种方法的优点和缺点。

2. Thread Safe HashSet Using ConcurrentHashMap Factory Method

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

Firstly we’ll look at the ConcurrentHashMap class that exposed the static newKeySet() method. Basically, this method returns an instance that respects the java.util.Set interface and allows the usage of standard methods like add(), contains(), etc.

首先我们来看看ConcurrentHashMap类,它暴露了静态newKeySet()方法。基本上,这个方法返回一个尊重java.util.Set接口的实例,并允许使用标准方法,如add(), contains(),等。

This can be created simply as:

这可以简单地创建为。

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

Furthermore, the performance of the returned Set is similar to the HashSet, as both are implemented using a hash-based algorithm. Moreover, the added overhead imposed by the synchronization logic is also minimal because the implementation uses a ConcurrentHashMap.

此外,返回的Set的性能与HashSet相似,因为两者都是使用基于hash的算法实现的。此外,由于该实现使用ConcurrentHashMap,同步逻辑所带来的额外开销也很小。

Lastly, the drawback is that the method exists only starting with Java 8.

最后,缺点是该方法仅从Java 8开始存在

3. Thread Safe HashSet Using ConcurrentHashMap Instance Methods

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

So far, we have looked at the static method of ConcurrentHashMap. Next, we’ll tackle the instance methods available for ConcurrentHashMap to create thread-safe Set instances. There are two methods available, newKeySet() and newKeySet(defaultValue) which slightly differ from each other.

到目前为止,我们已经看了ConcurrentHashMap的静态方法。接下来,我们将处理ConcurrentHashMap的实例方法,以创建线程安全的Set实例。有两个方法可用,newKeySet()newKeySet(defaultValue),它们彼此之间略有不同。

Both methods create a Set, which is linked with the original map. To put it differently, each time we add a new entry to the originating ConcurrentHashMap, the Set will receive that value. Further, let’s look at the differences between these two methods.

这两个方法都会创建一个Set,它与原始地图相联系。换句话说,每当我们向原始的ConcurrentHashMap添加一个新条目时,Set就会收到这个值。此外,让我们看看这两种方法的区别。

3.1. The newKeySet() Method

3.1.newKeySet()方法

As mentioned above, newKeySet() exposes a Set containing all keys of the originating map. The key difference between this method and the newKeySet(defaultValue) is that the current one doesn’t support adding new elements to the Set. So if we try to call methods like add() or addAll(), we’ll get an UnsupportedOperationException.

如上所述,newKeySet()暴露了一个Set,其中包含了源地图的所有键。这个方法和newKeySet(defaultValue)之间的关键区别在于,当前的方法不支持向Set添加新的元素。所以如果我们试图调用add()addAll()等方法,我们会得到一个UnsupportedOperationException。

Although operations like remove(object) or clear() are working as expected, we need to be aware that any change on the Set will be reflected in the original map:

尽管像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);

Next is the output of the code above:

接下来是上述代码的输出。

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. The newKeySet(defaultValue) Method

3.2.newKeySet(defaultValue)方法

Let’s look at another way to create a Set out of the keys in the map. Compared with the one mentioned above, newKeySet(defaultValue) returns a Set instance that supports adding of new elements by calling add() or addAll() on the set.

让我们看看另一种从地图中的键创建Set的方法。与上面提到的方法相比,newKeySet(defaultValue)返回一个Set实例,支持通过调用add()addAll()在该集合上添加新元素。

Further looking at the default value passed as a parameter, this is used as the value for each new entry in the map added thought add() or addAll() methods. The following example shows how this works:

进一步看作为参数传递的默认值,它被用作地图中每个新条目的值,这些新条目被认为是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);

Below is the output of the code above:

下面是上述代码的输出。

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. Thread Safe HashSet Using Collections Utility Class

4.使用Collections实用类的线程安全HashSet

Let’s use the synchronizedSet() method available in java.util.Collections to create a thread-safe HashSet instance:

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

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

Before using this approach, we need to be aware that it’s less efficient than the ones discussed above. Basically, synchronizedSet() just wraps the Set instance into a synchronized decorator compared with ConcurrentHashMap that implements a low-level concurrency mechanism.

在使用这种方法之前,我们需要注意,它的效率不如上面讨论的那些方法。基本上,synchronizedSet()只是将Set实例包装成一个同步的装饰器,与ConcurrentHashMap相比,它实现了一个低级别的并发机制。

5. Thread Safe Set Using CopyOnWriteArraySet

5.使用CopyOnWriteArraySet的线程安全Set

The last approach to create thread-safe Set implementations is the CopyOnWriteArraySet. Creating an instance of this Set is simple:

创建线程安全的Set实现的最后一种方法是CopyOnWriteArraySet。创建这个Set的实例很简单。

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

Although it looks appealing to use this class, we need to consider some serious performance drawbacks. Behind the scene, CopyOnWriteArraySet uses an Array, not a HashMap, to store the data. This means that operations like contains() or remove() have O(n) complexity, while when using Sets backed by ConcurrentHashMap, the complexity is O(1).

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

It is recommended is to use this implementation when the Set size stays generally small and read-only operations have a majority.

Set的大小通常较小,并且只读操作占多数时,建议使用这种实现。

6. Conclusions

6.结论

In this article, we have seen different possibilities to create thread-safe Set instances and emphasized the differences between them. Firstly we have seen the ConcurrentHashMap.newKeySet() static method. This should be the first choice when a thread-safe HashSet is needed. Afterwards we looked what are the differences between ConcurrentHashMap static method and newKeySet(), newKeySet(defaultValue)  for ConcurrentHashMap instances.

在这篇文章中,我们看到了创建线程安全的Set实例的不同可能性,并强调了它们之间的差异。首先我们看到了ConcurrentHashMap.newKeySet() 静态方法。当需要一个线程安全的HashSet时,这应该是第一选择。之后我们看了一下ConcurrentHashMap静态方法和newKeySet(), newKeySet(defaultValue)之间的区别,对于ConcurrentHashMap实例。

Finally we discussed also Collections.synchronizedSet() and the CopyOnWriteArraySet and there performance drawbacks.

最后我们还讨论了Collections.synchronizedSet()CopyOnWriteArraySet以及其性能缺陷。

As usual, the complete source code is available over on GitHub.

像往常一样,完整的源代码可以在GitHub上找到