Питання 21 · Розділ 4

Коли використовувати synchronized collections?

Уявіть туалет з одним ключем — поки один потік всередині, всі інші чекають в черзі, навіть якщо просто хочуть подивитися, чи вільно.

Мовні версії: English Russian Ukrainian

🟢 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

  1. Висока конкуренція (>10 потоків) — ConcurrentHashMap дасть на порядок кращий throughput
  2. Часті ітерації — кожна вимагає ручного synchronized(list) { ... }
  3. Складові операції (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

  1. НЕ використовуйте в новому коді
  2. ConcurrentHashMap — за замовчуванням
  3. CopyOnWriteArrayList — для listeners
  4. Ітерація → synchronized блок обов’язковий
  5. Складові операції → зовнішній synchronized
  6. Не експортуйте оригінальну колекцію
  7. 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]]