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

Ответ

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

Collections.synchronizedMap(Map<K,V> m)

  • Обертка (Wrapper): Статический метод, который возвращает синхронизированную обертку над переданной обычной Map (например, HashMap).
  • Глобальная блокировка: Для обеспечения потокобезопасности весь доступ к карте синхронизируется по одному объекту-монитору (по самой обертке). Это означает, что при любой операции (get, put, containsKey) блокируется вся карта, даже если потоки работают с разными ключами.
  • Итераторы: Требуют явной внешней синхронизации на самой карте во время итерации, иначе могут выбросить ConcurrentModificationException.
  • Null: Разрешает null для ключей и значений (если их разрешает базовая карта).
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

// Безопасный доступ к отдельным операциям
String value = syncMap.get("key"); // Внутри synchronized
syncMap.put("key", "value"); // Внутри synchronized

// Итерация ТРЕБУЕТ ручной синхронизации
synchronized (syncMap) {
    for (Map.Entry<String, String> entry : syncMap.entrySet()) {
        // работа с entry
    }
}

ConcurrentHashMap

  • Специализированная реализация: Построена с нуля для высокой параллельности.
  • Сегментированная блокировка (в Java 7) / Блокировки на уровне ячеек (Java 8+): Вместо одной блокировки на всю карту используются множество. В Java 8+ операции на разных ячейках (bucket) часто выполняются без блокировок, используя compare-and-swap (CAS).
  • Слабо-согласованные итераторы (weakly-consistent): Итератор отражает состояние карты на момент его создания или обхода, но никогда не бросает ConcurrentModificationException, даже если карта изменяется во время итерации. Он может показать, а может и не показать последние изменения.
  • Null: Запрещает null для ключей и значений (чтобы избежать двусмысленности в многопоточном контексте).
  • Специальные атомарные операции: Предоставляет методы putIfAbsent(), compute(), merge() и др., которые выполняются атомарно.
ConcurrentHashMap<String, String> concMap = new ConcurrentHashMap<>();

// Высокопроизводительный параллельный доступ
String value = concMap.get("key"); // Часто без блокировок
concMap.put("key", "value"); // Блокировка только на нужном сегменте/ячейке

// Безопасная итерация без внешней синхронизации
for (Map.Entry<String, String> entry : concMap.entrySet()) {
    // concMap может изменяться другим потоком - исключения не будет
}

// Атомарная операция
concMap.compute("key", (k, v) -> (v == null) ? "new" : v + "_updated");

Сравнительная таблица

Критерий synchronizedMap ConcurrentHashMap
Архитектура Обертка с глобальной блокировкой Внутренняя реализация с сегментированными/ячеечными блокировками/CAS
Масштабируемость Низкая (все потоки конкурируют за одну блокировку) Высокая (потоки работают с разными частями карты параллельно)
Итераторы Требуют внешней синхронизации, иначе ConcurrentModificationException Weakly-consistent, безопасны, не бросают исключений
Null-значения Разрешены Запрещены
Атомарные операции Нет (требуют внешней синхронизации) Да (putIfAbsent, compute, merge и др.)

Рекомендация: В современных многопоточных приложениях всегда предпочитайте ConcurrentHashMap. synchronizedMap следует использовать только для простых случаев с низкой конкуренцией или для обеспечения обратной совместимости с устаревшим кодом.

Ответ 18+ 🔞

Давай разберём эту дичь, как есть, без соплей. Смотри, тут две штуки, которые вроде как делают одно и то же — дают тебе Map, с которым можно работать из нескольких потоков и не получить пизды. Но внутри они устроены так по-разному, что это просто ёперный театр.

Collections.synchronizedMap(Map<K,V> m)

Представь себе здоровенного, лысого вышибалу по кличке "Синхрон".

  • Кто он? Это просто обёртка, банный лист, натянутый на обычную карту (типа HashMap). Старый, дедовский способ.
  • Как работает? У него один огромный замок на всю дверь. Хочешь зайти за ключом "хлеб", хочешь положить "молоко" — блядь, все стоят в одной очереди и ждут, пока предыдущий мудак выйдет. Одна блокировка на всю карту. Полный пиздец для производительности, если потоков больше двух.
  • Итераторы? А вот это вообще пизда. Если будешь бегать по нему циклом for, а в это время другой поток что-то туда сунет — получишь в лоб ConcurrentModificationException. Чтобы этого не было, надо самому, вручную, обернуть итерацию в synchronized. Заебашься.
  • Null? Да хуй с ним, разрешает, если базовая карта разрешает.
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());

// Тут внутри всё синхронизировано, но все лезут в одну дверь
String value = syncMap.get("key");
syncMap.put("key", "value");

// А вот тут уже твои проблемы. Забудешь synchronized — получишь сюрприз.
synchronized (syncMap) { // Без этой строчки — пиши пропало
    for (Map.Entry<String, String> entry : syncMap.entrySet()) {
        // работа с entry
    }
}

ConcurrentHashMap

А это уже не вышибала, а целая система умных турникетов.

  • Кто он? Специально спроектированная, навороченная карта для многопоточного ада. Не обёртка, а самостоятельная хуйня.
  • Как работает? В старых версиях (Java 7) карта делилась на сегменты со своими замками. В Java 8+ там вообще чёрная магия — блокировки на уровне отдельных ячеек, а часто и вообще обходятся без них, используя CAS-операции. Короче, потоки не толкаются в одной очереди, а спокойно работают с разными участками карты.
  • Итераторы? Красота. Они weakly-consistent. Это значит, итератор показывает тебе карту такой, какой он её застал, но если её в это время меняют, он не орёт, а просто может не показать свежие изменения. Никаких ConcurrentModificationException! Итерация без внешней синхронизации.
  • Null? А вот тут строго запрещены и ключи, и значения. Потому что в многопоточном коде null — это источник дичайших багов и неоднозначностей.
  • Фишки: Есть куча готовых атомарных операций вроде putIfAbsent, compute. Не надо самому изобретать велосипед с синхронизацией.
ConcurrentHashMap<String, String> concMap = new ConcurrentHashMap<>();

// Быстро, параллельно, без лишней драмы
String value = concMap.get("key");
concMap.put("key", "value");

// Итерируйся на здоровье, не боясь, что всё упадёт
for (Map.Entry<String, String> entry : concMap.entrySet()) {
    // Даже если в другом потоке карту меняют — исключения не будет
}

// Атомарная операция в одну строку — мечта
concMap.compute("key", (k, v) -> (v == null) ? "new" : v + "_updated");

Короче, таблица для тех, кто любит глазами

Что сравниваем synchronizedMap (Старый вышибала) ConcurrentHashMap (Умные турникеты)
Суть Глобальная блокировка на всю карту. Все стоят в одной очереди. Сегментированные/ячеечные блокировки. Много очередей.
Скорость под нагрузкой Хуёвая. Потоки друг другу мешают. Отличная. Потоки работают параллельно.
Итераторы Опасные. Требуют ручной синхронизации, иначе — исключение. Безопасные. Ничего не бросают, просто могут не всё показать.
Null-ы Можно. Нельзя! Иди нахуй со своим null.
Атомарные операции Нету. Делай сам. Есть из коробки (putIfAbsent, compute).

Итог, блядь: Если пишешь что-то новое и там больше одного потока — бери ConcurrentHashMap и не еби мозг. synchronizedMap — это для легаси-кода, для каких-то простейших случаев или когда тебе по какой-то ёбаной причине критично нужны null-значения. Во всех остальных случаях — ConcurrentHashMap, и точка.