Перейти к основному содержимому

Java Concurrent HashSet, эквивалентный ConcurrentHashMap

· 4 мин. чтения

1. Обзор

В этом руководстве мы увидим, какие есть возможности для создания потокобезопасных экземпляров HashSet и что является эквивалентом ConcurrentHashMap для HashSet . Кроме того, мы рассмотрим преимущества и недостатки каждого подхода.

2. Thread Safe HashSet с использованием фабричного метода ConcurrentHashMap

Во-первых, мы рассмотрим класс ConcurrentHashMap , который предоставляет статический метод newKeySet() . По сути, этот метод возвращает экземпляр, который соответствует интерфейсу java.util.Set и позволяет использовать стандартные методы, такие как add(), contains() и т. д.

Это может быть создано просто как:

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

Кроме того, производительность возвращаемого набора аналогична Has hSet , так как оба они реализованы с использованием алгоритма на основе хеширования. Кроме того, дополнительные накладные расходы, связанные с логикой синхронизации, также минимальны, поскольку реализация использует ConcurrentHashMap .

Наконец, недостатком является то, что метод существует только начиная с Java 8 .

3. Thread Safe HashSet с использованием методов экземпляра ConcurrentHashMap

До сих пор мы рассматривали статический метод ConcurrentHashMap. Далее мы рассмотрим методы экземпляра, доступные для ConcurrentHashMap , для создания потокобезопасных экземпляров Set . Доступны два метода, newKeySet() и newKeySet(defaultValue) , которые немного отличаются друг от друга.

Оба метода создают набор , связанный с исходной картой . Другими словами, каждый раз, когда мы добавляем новую запись в исходный ConcurrentHashMap, Set будет получать это значение. Далее, давайте рассмотрим отличия этих двух методов.

3.1. Метод newKeySet( )

Как упоминалось выше, newKeySet() предоставляет набор , содержащий все ключи исходной карты. Основное различие между этим методом и newKeySet(defaultValue) заключается в том, что текущий метод не поддерживает добавление новых элементов в Set . Поэтому, если мы попытаемся вызвать такие методы, как add() или addAll(), мы получим UnsupportedOperationException .

Хотя такие операции, как remove(object) или clear() , работают должным образом, мы должны знать, что любое изменение в наборе будет отражено в исходной карте:

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() для набора.

Далее, глядя на значение по умолчанию, переданное в качестве параметра, оно используется в качестве значения для каждой новой записи в карте, добавленной с помощью методов 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. Thread Safe HashSet с использованием служебного класса коллекций

Давайте воспользуемся методом synchronizedSet() , доступным в java.util.Collections , чтобы создать потокобезопасный экземпляр HashSet :

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

Прежде чем использовать этот подход, мы должны знать, что он менее эффективен, чем рассмотренные выше . По сути, synchronizedSet() просто оборачивает экземпляр Set в синхронизированный декоратор по сравнению с ConcurrentHashMap , который реализует низкоуровневый механизм параллелизма.

5. Потокобезопасный набор с помощью CopyOnWriteArraySet

Последний подход к созданию потокобезопасных реализаций Set — это CopyOnWriteArraySet . Создать экземпляр этого набора просто:

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

Хотя использование этого класса выглядит заманчиво, нам необходимо учитывать некоторые серьезные недостатки производительности. За кулисами CopyOnWriteArraySet использует для хранения данных массив, а не HashMap . Это означает, что такие операции, как contains() или remove() , имеют сложность O(n), в то время как при использовании наборов, поддерживаемых ConcurrentHashMap, сложность составляет O(1).

Рекомендуется использовать эту реализацию, когда размер набора остается, как правило, небольшим, а операции только для чтения составляют большинство.

6. Выводы

В этой статье мы рассмотрели различные возможности создания потокобезопасных экземпляров Set и подчеркнули различия между ними. Во-первых, мы видели статический метод ConcurrentHashMap.newKeySet() . Это должно быть первым выбором, когда требуется потокобезопасный HashSet . После этого мы посмотрели, в чем разница между статическим методом ConcurrentHashMap и newKeySet(), newKeySet(defaultValue) для экземпляров ConcurrentHashMap .

Наконец мы обсудили также Коллекции. synchronizedSet() и CopyOnWriteArraySet, `` а также недостатки производительности.

Как обычно, полный исходный код доступен на GitHub .