Java Collections.synchronizedMap() и ConcurrentHashMap предоставляют потокобезопасные реализации Map для использования в параллельных приложениях. В этом руководстве мы сосредоточимся на основных различиях между Collections.synchronizedMap() и ConcurrentHashMap.
1. Введение
Класс ConcurrentHashMap расширяет класс AbstractMap и реализует интерфейс ConcurrentMap и был представлен в версии Java 1.5. Класс HashMap не синхронизирован и может вызывать несогласованность данных, когда несколько потоков используют его одновременно. ConcurrentHashMap — это потокобезопасная альтернатива классу HashMap.
// ConcurrentHashMap class declarationpublic class ConcurrentHashMap <K, V> extends AbstractMap<K, V> implements Concurrent Map<K, V>, Serializable
synchronizedMap() — это метод в классе Collections, который был представлен в версии Java 1.2. Он возвращает синхронизированную карту, поддерживаемую указанной картой, которую мы предоставляем в параметре.
// synchronizedMap() methodpublic static Map<K, V> synchronizedMap(Map<K, V> map)
2. Различия между Collections.synchronizedMap() и ConcurrentHashMap
Давайте обсудим некоторые основные различия между этими двумя конструкциями.
2.1 Блокировка и параллелизм
Внутренне. ConcurrentHashMap использует внутреннюю сегментацию, называемую buckets. Он хранит все пары ключ-значение в этих buckets. По умолчанию он поддерживает 16 buckets.
ConcurrentHashMap позволяет неблокируемый доступ на чтение для всех потоков, но работающий поток должен получить блокировку на определенном контейнере перед записью в него данных. Обратите внимание, что ему не нужно блокировать весь объект карты, поэтому несколько потоков также могут одновременно писать.
Синхронизированная карта, созданная с помощью synchronizedMap(), обеспечивает последовательный доступ к базовой карте, поэтому только один поток может получить доступ к карте одновременно.
2.2. Пустые ключи и значения
ConcurrentHashMap внутренне использует HashTable в качестве базовой структуры данных; следовательно, он не допускает использования null в качестве ключа или значения.
При использовании synchronizedMap() поддержка null определяется резервной картой. Если резервная карта — это HashMap или LinkedHashMap, то мы можем вставить один ключ null и любое количество значений null в карту. Но если резервная карта — это TreeMap, то мы не можем вставить ключ null, но у нас все равно может быть любое количество значений null.
// Putting null in ConcurrentHashMapConcurrentHashMap<String, String> chmap = new ConcurrentHashMap<>();chmap.put(null, “value”); // NullPointerExceptionchmap.put(“key”, null); // NullPointerException// Putting null in SynchronizedMap obtained from synchronizedMap()Map<String, Integer> hmap = Collections.synchronizedMap(new HashMap<String, Integer>());hmap.put(null, 1); // AllowedMap<String, Integer> tmap = Collections.synchronizedMap(new TreeMap<String, Integer>());tmap.put(null, 1); // NullPointerException
2.3.Исключение ConcurrentModification
ConcurrentHashMap не выбрасывает ConcurrentModificationException, поэтому мы изменяем записи карты во время итерации карты, тогда итератор не выбрасывает это исключение. Итератор ConcurrentHashMap отказоустойчив, так как он не выбрасывает ConcurrentModificationException.
Итератор, полученный из synchronizedMap(), является отказоустойчивым и вызывает исключение ConcurrentModificationException.
2.4.Упорядочивание элементов
ConcurrentHashMap не поддерживает порядок своих элементов, т.е. не дает никаких гарантий, что элемент, вставленный первым в карту, будет выведен первым во время итерации карты.
Collections.synchronizedMap() поддерживается указанной картой и сохраняет порядок поддерживающей карты. Если мы передадим ей HashMap, то порядок не сохранится, но если мы передадим TreeMap, то он сохранит порядок элементов, как в TreeMap.
2.5.Производительность
- В экземпляре ConcurrentHashMap несколько потоков могут работать одновременно, чтобы безопасно выполнять параллельные операции чтения и записи, и благодаря этому ConcurrentHashMap работает лучше, чем другие синхронизированные карты.
- Collections.synchronizedMap() обеспечивает последовательный доступ, т.е. позволяет только одному потоку работать с ним для операции чтения или записи. Это увеличивает время ожидания других потоков и относительно снижает производительность карты.
Давайте сравним производительность обоих с помощью Java Microbenchmark Harness(JMH). Мы провели наши тесты производительности, используя 1 итерацию для 1000 элементов.
// Creating BenchmarkRunner class@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)@Fork(value = 1, warmups = 1)public class BenchmarkRunner{// For running the Benchmarkspublic static void main(String[] args) throws RunnerException{Options opt = new OptionsBuilder().include(BenchmarkRunner.class.getSimpleName()).forks(1).build();new Runner(opt).run();}// Creating Benchmark for synchronizedMap@Benchmarkpublic void synchronizedMapReadAndWrite(){Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());readAndWriteTest(map);}// Creating Benchmark for ConcurrentHashMap@Benchmarkpublic void concurrentHashMapReadAndWrite(){Map<String, Integer> map = new ConcurrentHashMap<>();readAndWriteTest(map);}// Test method for putting and getting random numbers from the mapprivate void readAndWriteTest(final Map<String, Integer> map){for(int i = 0; i < 1000; i++) {Integer randomNumber =(int) Math.ceil(Math.random() * 1000);map.get(String.valueOf(randomNumber));map.put(String.valueOf(randomNumber), randomNumber);}}}
Режим эталонного теста Cnt Оценка Ошибка ЕдиницыconcurrentHashMapReadAndWrite в среднем 5 0,146 0,047 мс/опsynchronizedMapReadAndWrite avgt 5 0,307 0,014 мс/оп
Взгляните на статистику бенчмарка: очевидно, что ConcurrentHashMap работает лучше как для операций чтения, так и для операций записи, чем synchronizedMap().
3. Варианты использования
Последовательный доступ, предоставляемый Collections.synchronizedMap(), гарантирует требуемую согласованность данных, если есть больше потоков для записи и только несколько потоков считывают данные. Таким образом, мы должны использовать синхронизированную карту, когда производительность не является основной проблемой, а согласованность данных.
В приложении, критически важном для производительности, мы должны использовать ConcurrentHashMap для лучшей производительности во время выполнения. Кроме того, если есть только несколько потоков, выполняющих запись, а остальные потоки считывают данные, то использование ConcurrentHashMap имеет больше смысла.
Кроме того, в многопоточной среде мы можем использовать ConcurrentHashMap вместо HashTable, поскольку последний является устаревшим классом и больше не рекомендуется.
4. Заключение
В этой статье мы рассмотрели основные различия между ConcurrentHashMap и Collections.synchronizedMap(), а также факторы, которые мы можем выбрать между ними при использовании их в нашем коде.