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

Collections.synchronizedMap против ConcurrentHashMap

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

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

В этом руководстве мы обсудим различия между Collections.synchronizedMap() и ConcurrentHashMap .

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

2. Различия

Collections.synchronizedMap() и ConcurrentHashMap обеспечивают потокобезопасные операции с коллекциями данных.

Вспомогательный класс Collections предоставляет полиморфные алгоритмы, которые работают с коллекциями и возвращают упакованные коллекции . Его метод synchronizedMap() обеспечивает потокобезопасную функциональность.

Как следует из названия, synchronizedMap() возвращает синхронизированную карту , поддерживаемую картой , которую мы предоставляем в параметре. Чтобы обеспечить потокобезопасность, synchronizedMap() разрешает все обращения к резервной карте через возвращаемую карту .

ConcurrentHashMap был представлен в JDK 1.5 как усовершенствование HashMap , которое поддерживает высокий параллелизм как для извлечения, так и для обновлений . HashMap не является потокобезопасным, поэтому это может привести к неправильным результатам во время конфликта потоков.

Класс ConcurrentHashMap является потокобезопасным. Таким образом, несколько потоков могут без проблем работать с одним объектом.

В ConcurrentHashMap операции чтения не блокируются, тогда как операции записи блокируют определенный сегмент или корзину. По умолчанию сегмент или уровень параллелизма равен 16, что означает, что 16 потоков могут писать в любой момент после блокировки сегмента или сегмента.

2.1. ConcurrentModificationException

Для таких объектов, как HashMap , выполнение параллельных операций не допускается. Поэтому, если мы попытаемся обновить HashMap во время итерации по нему, мы получим ConcurrentModificationException . Это также произойдет при использовании synchronizedMap() :

@Test(expected = ConcurrentModificationException.class)
public void whenRemoveAndAddOnHashMap_thenConcurrentModificationError() {
Map<Integer, String> map = new HashMap<>();
map.put(1, "foreach");
map.put(2, "HashMap");
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
Iterator<Entry<Integer, String>> iterator = synchronizedMap.entrySet().iterator();
while (iterator.hasNext()) {
synchronizedMap.put(3, "Modification");
iterator.next();
}
}

Однако это не относится к ConcurrentHashMap :

Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "foreach");
map.put(2, "HashMap");

Iterator<Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
map.put(3, "Modification");
iterator.next()
}

Assert.assertEquals(3, map.size());

2.2. нулевая поддержка

Collections.synchronizedMap() и ConcurrentHashMap по-разному обрабатывают нулевые ключи и значения .

ConcurrentHashMap не допускает null в ключах или значениях:

@Test(expected = NullPointerException.class)
public void allowNullKey_In_ConcurrentHasMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put(null, 1);
}

Однако при использовании Collections.synchronizedMap() поддержка null зависит от входной Map . У нас может быть один нуль в качестве ключа и любое количество нулевых значений, когда Collections.synchronizedMap() поддерживается HashMap или LinkedHashMap, тогда как, если мы используем TreeMap , у нас могут быть нулевые значения, но не нулевые ключи.

Давайте утверждаем, что мы можем использовать нулевой ключ для Collections.synchronizedMap() , поддерживаемый HashMap :

Map<String, Integer> map = Collections
.synchronizedMap(new HashMap<String, Integer>());
map.put(null, 1);
Assert.assertTrue(map.get(null).equals(1));

Точно так же мы можем проверить поддержку null в значениях для Collections.synchronizedMap() и ConcurrentHashMap .

3. Сравнение производительности

Давайте сравним производительность ConcurrentHashMap и Collections.synchronizedMap(). В этом случае мы используем платформу с открытым исходным кодом Java Microbenchmark Harness (JMH) для сравнения производительности методов в наносекундах .

Мы провели сравнение случайных операций чтения и записи на этих картах. Давайте быстро взглянем на наш тестовый код JMH:

@Benchmark
public void randomReadAndWriteSynchronizedMap() {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
performReadAndWriteTest(map);
}

@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
performReadAndWriteTest(map);
}

private void performReadAndWriteTest(final Map<String, Integer> map) {
for (int i = 0; i < TEST_NO_ITEMS; i++) {
Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
map.get(String.valueOf(randNumber));
map.put(String.valueOf(randNumber), randNumber);
}
}

Мы выполнили наши тесты производительности, используя 5 итераций с 10 потоками для 1000 элементов. Посмотрим результаты тестов:

Benchmark                                                     Mode  Cnt        Score        Error  Units
MapPerformanceComparison.randomReadAndWriteConcurrentHashMap avgt 100 3061555.822 ± 84058.268 ns/op
MapPerformanceComparison.randomReadAndWriteSynchronizedMap avgt 100 3234465.857 ± 60884.889 ns/op
MapPerformanceComparison.randomReadConcurrentHashMap avgt 100 2728614.243 ± 148477.676 ns/op
MapPerformanceComparison.randomReadSynchronizedMap avgt 100 3471147.160 ± 174361.431 ns/op
MapPerformanceComparison.randomWriteConcurrentHashMap avgt 100 3081447.009 ± 69533.465 ns/op
MapPerformanceComparison.randomWriteSynchronizedMap avgt 100 3385768.422 ± 141412.744 ns/op

Приведенные выше результаты показывают, что ConcurrentHashMap работает лучше, чем Collections.synchronizedMap() .

4. Когда использовать

Мы должны отдавать предпочтение Collections.synchronizedMap() , когда согласованность данных имеет первостепенное значение, и мы должны выбирать ConcurrentHashMap для критически важных приложений, где операций записи гораздо больше, чем операций чтения.

Это связано с тем, что Collections.synchronizedMap() требует, чтобы каждый поток блокировал весь объект как для операций чтения, так и для записи. Для сравнения, ConcurrentHashMap позволяет потокам блокировать отдельные сегменты коллекции и одновременно вносить изменения.

5. Вывод

В этой статье мы продемонстрировали различия между ConcurrentHashMap и Collections.synchronizedMap() . Мы также продемонстрировали производительность обоих из них, используя простой тест JMH.

Как всегда, образцы кода доступны на GitHub .