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

«В чем разница между Collections.synchronizedMap и ConcurrentHashMap?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Обе реализации предоставляют потокобезопасный 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 следует использовать только для простых случаев с низкой конкуренцией или для обеспечения обратной совместимости с устаревшим кодом.