В чём разница между ConcurrentHashMap и Collections.synchronizedMap в Java

Ответ

Обе структуры обеспечивают потокобезопасность, но с фундаментально разной архитектурой, что влияет на производительность и поведение.

Основные различия

  1. Гранулярность блокировки

    • ConcurrentHashMap: Использует сегментированные блокировки (в старых версиях) или оптимистичные блокировки (CAS-операции) на уровне отдельных бакетов (в Java 8+). Это позволяет множеству потоков параллельно читать и даже записывать в разные сегменты.
    • Collections.synchronizedMap(Map): Создает обертку, которая синхронизирует все методы исходной мапы с помощью одной общей блокировки (synchronized). На время любой операции (даже get()) вся мапа блокируется для других потоков.
  2. Согласованность итераторов

    • ConcurrentHashMap: Итераторы обладают слабой согласованностью (weakly consistent). Они не бросают ConcurrentModificationException и могут отражать часть, но не обязательно все изменения, произошедшие после их создания.
    • synchronizedMap: Для безопасного перебора требуется явная внешняя синхронизация на самом объекте мапы, иначе возможно исключение ConcurrentModificationException.

Пример использования

// ConcurrentHashMap — высокая производительность при конкурентном доступе
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key", 42); // Не требует внешней синхронизации
Integer value = concurrentMap.get("key");

// synchronizedMap — простое, но менее производительное решение
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Для итерации все равно нужна ручная синхронизация
synchronized (syncMap) {
    for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
        // обработка entry
    }
}

Когда что использовать?

  • ConcurrentHashMap: Практически всегда для высоконагруженных многопоточных приложений. Это стандартный выбор.
  • Collections.synchronizedMap(): Только если нужна простая синхронизация для мапы с очень низкой конкуренцией записей или для совместимости с кодом, требующим полной синхронизации итераторов.

Ответ 18+ 🔞

А, ну это же классика, про которую каждый джуниор на собеседовании обосраться может! Смотри, тут две штуки, которые вроде как одно и то же делают — потокобезопасную мапу из тебя делают, — но внутри них адски разная кухня, и от этого пиздец как разное поведение и скорость.

Короче, в чём соль

  1. Кто на ком стоит, или гранулярность

    • ConcurrentHashMap: Этот хитрожопый. Он не душит всю мапу одним большим замком. В старых версиях он её на сегменты резал и каждый сегмент своим замком прикрывал. А в новых, с Java 8, там вообще красота — на уровне отдельных корзинок (бакетов) используются CAS-операции, это такие атомарные штуки без тяжёлых блокировок. Итог: много потоков могут одновременно и читать, и даже писать в разные части мапы, не мешая друг другу. Красота!
    • Collections.synchronizedMap(Map): А это тупой и сильный, как медведь. Берёт твою обычную мапу (хоть HashMap) и оборачивает её в такой кокон, где каждый методput, get, size — обвешан одним общим synchronized. Пока один поток внутри, все остальные ждут у двери, даже если просто значение прочитать пришли. Полная блокада на время любой операции.
  2. Итераторы, или как по мапе гулять

    • ConcurrentHashMap: Итераторы у него — слабой согласованности. Это мудрёное словосочетание значит: создал ты итератор, пошёл по нему, а в это время другой поток что-то в мапу записал или удалил. Так вот, итератор не взорвётся с ConcurrentModificationException. Он может показать тебе часть новых изменений, а может и не показать — как повезёт. Но исключения не будет, и программа не сломается.
    • synchronizedMap: Тут всё по-старинке, строго и опасно. Чтобы безопасно итерироваться, тебе нужно вручную, своими кривыми ручками, синхронизировать весь блок кода на самом объекте мапы. Забыл — получи ConcurrentModificationException в самый неподходящий момент и хуй пойми, откуда он вылез.

Смотри, как это в коде выглядит

// ConcurrentHashMap — для взрослых, кто гонит за производительностью
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("ключ", 42); // Пиши спокойно, без нервов
Integer val = chm.get("ключ"); // Читай тоже спокойно

// synchronizedMap — для лёгких случаев или легаси, где похуй на скорость
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// А вот итерация — тут надо голову включать! Без этого synchronized — пиздец.
synchronized (syncMap) { // Вот этот замок ОБЯЗАТЕЛЕН!
    for (Map.Entry<String, Integer> entry : syncMap.entrySet()) {
        // что-то делаем с entry
    }
}

Так что же выбрать, ёпта?

  • ConcurrentHashMap: Почти всегда. Серьёзно. Если у тебя многопоточность и хоть какая-та нагрузка — это твой выбор. Он быстрее, умнее, современнее. Стандарт де-факто.
  • Collections.synchronizedMap(): Очень редко. Ну, допустим, у тебя мапа почти не меняется, записей раз в час, а читают её изредка. Или ты прикручиваешься к какому-то старому говнокоду, который требует именно полной синхронизации итераторов. Во всех остальных случаях — это просто тормозной и неудобный костыль.