← Назад к вопросам

Какие плюсы и минусы Collections.synchronizedMap?

2.0 Middle🔥 71 комментариев
#Коллекции#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Collections.synchronizedMap: плюсы и минусы

Collections.synchronizedMap() — это один из способов создать потокобезопасную Map. Но он имеет серьёзные ограничения, которые нужно понимать.

Что такое Collections.synchronizedMap?

Map<String, String> synchronizedMap = Collections.synchronizedMap(
    new HashMap<>()
);

// Эквивалентно:
Map<String, String> synchronizedMap = new HashMap<>();
// Все методы могут быть вызваны одновременно из разных потоков

Как это работает?

Внутри Collections.synchronizedMap создаёт wrapper (обёртку) вокруг исходной Map и синхронизирует каждый вызов метода.

private static class SynchronizedMap<K,V> implements Map<K,V> {
    final Map<K,V> m;     // Исходная Map
    final Object mutex;   // Объект для синхронизации
    
    @Override
    public V get(Object key) {
        synchronized(mutex) {  // СИНХРОНИЗАЦИЯ ЗДЕСЬ!
            return m.get(key);
        }
    }
    
    @Override
    public V put(K key, V value) {
        synchronized(mutex) {  // И ЗДЕСЬ!
            return m.put(key, value);
        }
    }
}

Плюсы

1. Простота использования

Map<String, Integer> syncMap = Collections.synchronizedMap(
    new HashMap<>()
);

// Просто используем как обычную Map
syncMap.put("key", 1);
Integer value = syncMap.get("key");

Плюс: не нужно вручную писать synchronized блоки.

2. Требует минимальных изменений

// БЫЛО:
Map<String, Integer> map = new HashMap<>();

// СТАЛО (одна строка):
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());

Плюс: просто обернуть существующий код.

3. Работает для всех типов Map

// Работает с любой реализацией Map
Map<String, Integer> syncLinkedHashMap = Collections.synchronizedMap(
    new LinkedHashMap<>()
);

Map<String, Integer> syncTreeMap = Collections.synchronizedMap(
    new TreeMap<>()
);

4. Синхронизирует ТОЛЬКО публичные методы

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

Минусы

1. Минусы Синхронизация на КАЖДЫЙ вызов

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Каждый вызов — это отдельная синхронизация!
syncMap.put("key1", 1);  // synchronized блок
syncMap.put("key2", 2);  // synchronized блок (ждёт первого)
syncMap.put("key3", 3);  // synchronized блок (ждёт второго)

// Это медленно для частых операций

Минус: низкая производительность под нагрузкой.

2. Composite операции НЕ атомарны

Это критическая проблема!

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Плохой пример: Race condition
if (!syncMap.containsKey("key")) {  // Синхронизирован
    syncMap.put("key", 1);           // Синхронизирован
}  // НО! Между containsKey и put может вставить другой поток!

// Сценарий гонки:
Thread 1: проверяет containsKey("key") -> false
Thread 2: проверяет containsKey("key") -> false
Thread 1: вставляет "key" -> 1
Thread 2: вставляет "key" -> 2  // Перезапишет!
Результат: оба потока прошли проверку и вставили значение

// ХОРОШО: используй putIfAbsent
Integer result = syncMap.putIfAbsent("key", 1);  // Одна атомарная операция

3. Итерация требует ручной синхронизации

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// НЕПРАВИЛЬНО: ConcurrentModificationException
for (String key : syncMap.keySet()) {
    syncMap.put(key + "_new", 0);  // Может вызвать исключение
}

// ПРАВИЛЬНО: ручная синхронизация
synchronized(syncMap) {  // Нужно знать о mutex!
    for (String key : syncMap.keySet()) {
        syncMap.put(key + "_new", 0);
    }
}

// ЛУЧШЕ: используй ConcurrentHashMap
Map<String, Integer> concMap = new ConcurrentHashMap<>(syncMap);
for (String key : concMap.keySet()) {
    concMap.put(key + "_new", 0);  // Не нужна синхронизация
}

4. Глубокий pessimistic locking

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Во время get() вся Map заблокирована!
Integer value = syncMap.get("key1");  // Другие потоки ждут

// Это неправильно для читающих операций
// (read-heavy workloads)

Минус: даже читающие операции блокируют друг друга.

5. No fine-grained locking

// ConcurrentHashMap делает это лучше:
// Разные bucket'ы могут быть заблокированы независимо
// (segment-based locking или bucket-based locking в Java 8+)

ConcurrentHashMap<String, Integer> concMap = new ConcurrentHashMap<>();

// Thread 1 пишет в bucket 1, Thread 2 пишет в bucket 2
// Они НЕ ждут друг друга!

Минус: lower throughput под конкурентными нагрузками.

6. Нет гарантий на составные операции

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Нет atomic операций
syncMap.get("key");           // потокобезопасно
syncMap.put("key", 2);         // потокобезопасно

// Но не:
syncMap.putIfAbsent("key", 1);  // Работает в ConcurrentHashMap
syncMap.compute("key", (k, v) -> v + 1);  // Работает в ConcurrentHashMap

7. Deadlock risk в некоторых случаях

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

// Если callback (например, в stream) пытается обращаться к syncMap
syncMap.values().forEach(v -> {
    syncMap.put("key_new", v);  // Может вызвать deadlock!
});

Сравнение: synchronizedMap vs ConcurrentHashMap

┌─────────────────────────┬──────────────────────┬─────────────────────┐
│ Характеристика          │ synchronizedMap      │ ConcurrentHashMap   │
├─────────────────────────┼──────────────────────┼─────────────────────┤
│ Thread-safe             │ Да                   │ Да                  │
│ Производительность      │ Низкая               │ Высокая             │
│ Read performance        │ Низкая               │ Очень высокая       │
│ Write performance       │ Низкая               │ Высокая             │
│ Lock mechanism          │ Global mutex         │ Bucket-level locks  │
│ Atomic operations       │ Нет                  │ Да (compute, etc)   │
│ Null-safe              │ Да                   │ Нет (реже нужно)    │
│ Итерация безопасна      │ Нет (требует sync)   │ Да                  │
│ putIfAbsent            │ Нет (требует check)  │ Да (atomic)         │
│ Старый код (pre-Java5) │ Да (раньше это было) │ Предпочитаемо        │
└─────────────────────────┴──────────────────────┴─────────────────────┘

Правильное использование ConcurrentHashMap вместо synchronizedMap

// БЫЛО (плохо):
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());

if (!map.containsKey("key")) {
    map.put("key", 1);
}

for (String key : map.keySet()) {  // Требует ручной синхронизации!
    System.out.println(key);
}

// СТАЛО (хорошо):
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// Атомарная операция
map.putIfAbsent("key", 1);

// Итерация безопасна!
for (String key : map.keySet()) {
    System.out.println(key);
}

// Другие удобства:
map.compute("key", (k, v) -> v == null ? 0 : v + 1);
map.computeIfAbsent("key2", k -> 0);
map.getOrDefault("key3", 0);

Практические примеры проблем

Пример 1: Race Condition

Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
int threadCount = 100;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);

for (int i = 0; i < threadCount; i++) {
    executor.submit(() -> {
        if (!syncMap.containsKey("counter")) {
            syncMap.put("counter", 0);
        }
        Integer value = syncMap.get("counter");
        syncMap.put("counter", value + 1);
        latch.countDown();
    });
}

latch.await();
system.out.println("Value: " + syncMap.get("counter"));
// Ожидаем: 100
// Реальность: может быть 50, 75, 90... (race condition!)

// Правильно с ConcurrentHashMap:
ConcurrentHashMap<String, Integer> concMap = new ConcurrentHashMap<>();
for (int i = 0; i < threadCount; i++) {
    executor.submit(() -> {
        concMap.compute("counter", (k, v) -> v == null ? 1 : v + 1);
        latch.countDown();
    });
}
latch.await();
system.out.println("Value: " + concMap.get("counter"));  // Всегда 100!

Выводы

Collections.synchronizedMap:

Плюсы:

  • Простота использования (одна строка)
  • Потокобезопасность для отдельных операций
  • Работает со старым кодом

Минусы:

  • Низкая производительность
  • Нет атомарных составных операций
  • Требует ручной синхронизации при итерации
  • Race conditions при composite operations
  • Неправильно для modern Java (используй ConcurrentHashMap)

Рекомендация:

НЕ используй Collections.synchronizedMap в production коде!

Вместо этого используй:

  • ConcurrentHashMap — стандартный выбор для многопоточного кода
  • StampedLock — для специальных случаев
  • ReadWriteLock — если много читающих операций
  • Synchronized блоки — если нужна сложная синхронизация

synchronizedMap остался в Java только для backward compatibility. Это устаревший подход.

Какие плюсы и минусы Collections.synchronizedMap? | PrepBro