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 .