← Назад к вопросам
В чём разница между 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.synchronizedMap | ConcurrentHashMap |
|---|---|---|
| Механизм | Один большой 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 если:
- Очень редко (практически никогда)
- Кеш-код (legacy code)
- Минимальная нагрузка
// Не рекомендуется, но иногда встречается
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Используй ConcurrentHashMap если:
- Многопоточный доступ (99% случаев)
- Нужна высокая производительность
- Нужна 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 для многопоточных приложений.