Когда использовать synchronized collections?
Представьте туалет с одним ключом — пока один поток внутри, все остальные ждут в очереди, даже если просто хотят посмотреть, свободен ли он.
🟢 Junior Level
Synchronized collections — обычные коллекции с блокировкой на каждый метод.
Представьте туалет с одним ключом — пока один поток внутри, все остальные ждут в очереди, даже если просто хотят посмотреть, свободен ли он.
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Проблема: Только ОДИН поток может работать одновременно (даже читать!).
Современная альтернатива:
// ❌ Старый подход (медленный)
List<String> list = Collections.synchronizedList(new ArrayList<>());
// ✅ Новый подход (быстрый)
Map<String, Integer> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();
synchronizedList — один lock на ВСЕ операции, включая чтение. ConcurrentHashMap — lock-free чтение и гранулярные локи на запись, поэтому N потоков читают параллельно.
🟡 Middle Level
Проблема: один лок на всё
// 100 потоков хотят прочитать
syncList.get(0); // Все ждут в очереди!
// → 0 параллелизма
Составные операции НЕ атомарны
// ❌ Race condition!
if (!syncList.contains(item)) { // Поток A проверил
// Поток B вклинился и добавил
syncList.add(item); // Поток A добавил дубликат!
}
// ✅ Внешний synchronized
synchronized (syncList) {
if (!syncList.contains(item)) {
syncList.add(item);
}
}
Итерация требует ручного лока
// ❌ ConcurrentModificationException!
for (String s : syncList) { ... }
// ✅ Правильно:
synchronized (syncList) {
for (String s : syncList) { ... }
}
Когда использовать
| Сценарий | Что выбрать |
|---|---|
| Новый код | ConcurrentHashMap |
| Списки слушателей | CopyOnWriteArrayList |
| Полная блокировка | synchronizedList |
| Legacy код | synchronizedList |
COWAL хорош для слушателей: итерация (обход всех listeners) чаще, чем добавление listener. COWAL даёт CME-free итерацию и O(1) добавление.
🔴 Senior Level
Когда НЕ использовать synchronized collections
- Высокая конкуренция (>10 потоков) — ConcurrentHashMap даст на порядок лучший throughput
- Частые итерации — каждая требует ручного
synchronized(list) { ... } - Составные операции (check-then-act) — всё равно нужен внешний synchronized, так зачем обёртка?
Coarse-grained locking
Один монитор на ВСЮ коллекцию:
→ 100 читателей → сериализация
→ На многоядерных CPU → 0 масштабируемости
ConcurrentHashMap:
→ Lock-free чтение
→ CAS + synchronized на корзину
→ 100 потоков работают параллельно
Double synchronization
// ❌ Двойная блокировка!
synchronized (syncList) {
syncList.add(e); // Внутренний synchronized + внешний
}
// synchronizedList уже синхронизирует каждый метод
// → Лишний overhead
Экспорт оригинальной коллекции
// ❌ Нарушение thread-safety!
List<String> original = new ArrayList<>();
List<String> sync = Collections.synchronizedList(original);
// Изменение в обход обёртки:
original.add("x"); // НЕ синхронизировано!
Когда synchronized collections оправданы
// 1. Legacy системы (код до Java 5)
// 2. Грубая атомарность всей коллекции
synchronized (syncList) {
// Гарантированно никто не вклинится
for (var item : syncList) {
process(item);
}
}
// 3. Низкая конкуренция + малый размер
// → Обертка дешевле ConcurrentHashMap
Production Experience
Реальный сценарий: synchronizedMap убил throughput
- API: 1000 RPS, synchronizedMap для кэша
- Lock contention: 90% времени на synchronized
- Решение: ConcurrentHashMap
- Результат: +900% throughput
Best Practices
- НЕ используйте в новом коде
- ConcurrentHashMap — по умолчанию
- CopyOnWriteArrayList — для listeners
- Итерация → synchronized блок обязателен
- Составные операции → внешний synchronized
- Не экспортируйте оригинальную коллекцию
- Vector/Hashtable → избегайте в новом коде. Единственный случай, когда терпимы — single-threaded legacy-код, где замена не оправдана рисками.
Резюме для Senior
- Один лок → 0 масштабируемости
- Составные операции → race condition без внешнего synchronized
- Итерация → ручной synchronized блок
- ConcurrentHashMap > synchronizedMap в 99% случаев
- Legacy → единственное оправдание
- Vector/Hashtable → встроенные synchronized → legacy
🎯 Шпаргалка для интервью
Обязательно знать:
- synchronized collections — одна блокировка на все методы (чтение + запись)
- Составные операции (check-then-act) НЕ атомарны — нужен внешний
synchronized - Итерация требует ручного
synchronized(list) { ... }блока - ConcurrentHashMap даёт lock-free чтение и гранулярные локи на запись
- CopyOnWriteArrayList идеален для списков слушателей (частое чтение, редкая запись)
- В новом коде synchronized collections не рекомендуются
- Экспорт оригинальной коллекции нарушает thread-safety
- Vector/Hashtable — legacy, избегать в новом коде
Частые уточняющие вопросы:
- Почему synchronizedMap убивает throughput при 1000 RPS? — 90% времени тратится на lock contention; ConcurrentHashMap решает проблему.
- Атомарна ли конструкция
if (!list.contains(x)) list.add(x)? — Нет, это два отдельных вызова; нужен внешнийsynchronized(list). - Почему итерация по synchronizedList без synchronized блока выбросит CME? — Итерация — составная операция (hasNext + next), не защищена одним вызовом метода.
- Когда synchronized collections оправданы? — Legacy код до Java 5, низкая конкуренция, грубая атомарность всей коллекции.
Красные флаги (НЕ говорить):
- «synchronizedList — хороший выбор для нового кода» — нет, ConcurrentHashMap/CopyOnWriteArrayList лучше
- «Составные операции атомарны в synchronizedList» — нет, нужны внешние synchronized блоки
- «Vector — нормальный выбор» — это legacy класс с грубой блокировкой
- «synchronizedList решает все проблемы конкурентности» — не решает race condition для составных операций
Связанные темы:
- [[22. Как получить synchronized коллекцию]]
- [[26. Что такое fail-fast и fail-safe итераторы]]
- [[27. Что такое ConcurrentModificationException]]