Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:


1. Overview

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.

2. Thread Safe HashSet Using ConcurrentHashMap Factory Method

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.

This can be created simply as:

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

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.

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

3. Thread Safe HashSet Using ConcurrentHashMap Instance Methods

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.

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.

3.1. The newKeySet() Method

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.

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:

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);


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

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.

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:

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);


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

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

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

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.

5. Thread Safe Set Using CopyOnWriteArraySet

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

Set<Integer> copyOnArraySet = new CopyOnWriteArraySet<>();

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).

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

6. Conclusions

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.

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

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

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

Generic footer banner
Comments are closed on this article!