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

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

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

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

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

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

Разница между ConcurrentHashMap и Collections.synchronizedMap

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

Collections.synchronizedMap

Принцип: Блокирует ВСЮ карту (грубая синхронизация).

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

// Все операции блокируют ВЕСЬ объект
syncMap.put("key", "value");  // Блокирует весь syncMap
syncMap.get("key");           // Ждёт, пока освободится весь объект

Как это работает (внутри):

// Упрощённо, так работает Collections.synchronizedMap
class SynchronizedMapWrapper<K, V> implements Map<K, V> {
    private final Map<K, V> delegate;
    
    @Override
    public V put(K key, V value) {
        synchronized (this) {  // Блокирует ВСЮ карту!
            return delegate.put(key, value);
        }
    }
    
    @Override
    public V get(Object key) {
        synchronized (this) {  // Ждёт освобождения
            return delegate.get(key);
        }
    }
}

Проблема:

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

// Сценарий: 100 потоков хотят прочитать данные
for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        String value = map.get("key");  // ВСЕ блокируют друг друга!
        // Даже для READ операций!
    }).start();
}

ConcurrentHashMap

Принцип: Блокирует только нужный сегмент (fine-grained синхронизация).

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

// Разные потоки могут одновременно работать с разными сегментами
concMap.put("key1", "value1");  // Блокирует сегмент 1
concMap.put("key2", "value2");  // Блокирует сегмент 2 (одновременно!)
concMap.get("key1");             // Может работать с сегментом 1?

Как это работает (упрощённо):

class ConcurrentHashMap<K, V> {
    private Node<K, V>[] buckets;  // Массив сегментов
    private ReentrantLock[] locks;   // Разные блокировки для каждого сегмента
    
    public V put(K key, V value) {
        int hash = hash(key);
        int segment = hash % buckets.length;
        
        locks[segment].lock();  // Блокирует только сегмент
        try {
            // Работа с одним сегментом
            return putInSegment(segment, key, value);
        } finally {
            locks[segment].unlock();
        }
    }
}

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

АспектCollections.synchronizedMapConcurrentHashMap
МеханизмОдин большой lock на всю картуНесколько locks на сегменты
ПроизводительностьНизкая при большой нагрузкеВысокая
Одновременные операцииОднаНесколько (разные сегменты)
Read операцииБлокируют друг другаНе блокируют
Write операцииБлокируют друг другаБлокируют только свой сегмент
ScalabilityПлохаяХорошая
NullПозволяетНе позволяет (исключение)
IterationТребует синхронизацииБезопасна

Графическое объяснение

Collections.synchronizedMap:

Карта: [сегмент1] [сегмент2] [сегмент3] [сегмент4]
       ^________________________lock_________________________^
       
Попытка доступа Thread 1: БЛОКИРОВКА
Попытка доступа Thread 2: ЖДЁТ (даже к другому сегменту!)
Попытка доступа Thread 3: ЖДЁТ
Попытка доступа Thread 4: ЖДЁТ

ConcurrentHashMap:

Карта: [сегмент1] [сегмент2] [сегмент3] [сегмент4]
       ^lock1^    ^lock2^    ^lock3^    ^lock4^
       
Thread 1 работает с сегментом1: РАБОТАЕТ
Thread 2 работает с сегментом2: РАБОТАЕТ (одновременно!)
Thread 3 работает с сегментом3: РАБОТАЕТ (одновременно!)
Thread 4 работает с сегментом4: РАБОТАЕТ (одновременно!)

Пример производительности

@Benchmark
public void synchronizedMapWrite() {
    Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
    for (int i = 0; i < 1000; i++) {
        map.put("key" + i, i);
    }
}
// Результат: ~500 микросекунд

@Benchmark
public void concurrentHashMapWrite() {
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    for (int i = 0; i < 1000; i++) {
        map.put("key" + i, i);
    }
}
// Результат: ~50 микросекунд (в 10 раз быстрее!)

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

Используй Collections.synchronizedMap если:

  1. Очень редко (практически никогда)
  2. Кеш-код (legacy code)
  3. Минимальная нагрузка
// Не рекомендуется, но иногда встречается
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

Используй ConcurrentHashMap если:

  1. Многопоточный доступ (99% случаев)
  2. Нужна высокая производительность
  3. Нужна scalability
// ✅ Правильный выбор
ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();

public class CacheService {
    private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();
    
    public void cacheUser(String id, User user) {
        users.put(id, user);  // Быстро
    }
    
    public User getUser(String id) {
        return users.get(id);  // Не блокирует других
    }
}

Специфика ConcurrentHashMap

1. Null-ы НЕ разрешены

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key", null);  // Выбросит NullPointerException
map.get("key");        // Выбросит

// HashMap позволяет
map = new HashMap<>();
map.put("key", null);  // OK

2. Итерация безопасна, но может показать неполные данные

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("a", "1");
map.put("b", "2");
map.put("c", "3");

// Iterator безопасен (нет ConcurrentModificationException)
for (String key : map.keySet()) {
    map.put("d", "4");  // Может и не появиться в этой итерации
}

3. Операции вроде putIfAbsent работают атомарно

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

// Это АТОМАРНАЯ операция
// Гарантирует: либо вставит, либо вернёт существующее значение
String result = map.putIfAbsent("key", "value");

// Вместо synchronized варианта:
String value;
synchronized (map) {
    if (map.containsKey("key")) {
        value = map.get("key");
    } else {
        map.put("key", "value");
        value = "value";
    }
}

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

@Service
public class UserCacheService {
    // ✅ ConcurrentHashMap для кэша
    private final ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();
    
    public User getOrLoad(String userId) {
        // Если нет в кэше, загружаем
        return cache.computeIfAbsent(userId, id -> {
            return userRepository.findById(id);
        });
        // Несколько потоков могут вызвать это одновременно
        // Каждый получит свой результат, но в разных сегментах
    }
    
    public void invalidate(String userId) {
        cache.remove(userId);
    }
}

Вывод

Collections.synchronizedMap:

  • Один большой lock на всю карту
  • Низкая производительность
  • Никогда не используй в production

ConcurrentHashMap:

  • Locks на отдельные сегменты
  • Высокая производительность
  • Всегда используй вместо synchronized HashMap

Правило: Если тебе нужна потокобезопасная карта, используй ConcurrentHashMap. Это стандарт Java для многопоточных приложений.