Коли використовувати 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]]