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

Як отримати synchronized колекцію?

4. Потік B отримує доступ

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

Synchronized колекція — це обгортка (decorator) навколо звичайної колекції, яка обгортає кожен метод в synchronized-блок. Це гарантує, що одночасно тільки один потік може викликати будь-який метод колекції.

🟢 Junior Level

Спосіб через Collections.synchronizedX:

List<String> list = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());

ВАЖЛИВО: ітерація вимагає synchronized!

// ❌ Неправильно
for (String s : syncList) { ... }

// ✅ Правильно
synchronized (syncList) {
    for (String s : syncList) { ... }
}

🟡 Middle Level

Фабричні методи

Collections.synchronizedList(list)
Collections.synchronizedMap(map)
Collections.synchronizedSet(set)
Collections.synchronizedNavigableMap(navigableMap)  // Java 8+

Механіка: Decorator патерн

// Всередині:
class SynchronizedList<E> implements List<E> {
    final List<E> list;
    final Object mutex;  // = this за замовчуванням

    public synchronized E get(int index) {
        return list.get(index);  // synchronized на mutex
    }
}

Що відбувається при конкурентному доступі:

  1. Потік A викликає get(0) → захоплює монітор mutex
  2. Потік B викликає get(1) → блокується на synchronized, чекає
  3. Потік A завершує → звільняє монітор
  4. Потік B отримує доступ

Порівняння з альтернативами

Спосіб Коли
Collections.synchronizedX Потрібне повне блокування
ConcurrentHashMap Highload, часті записи
CopyOnWriteArrayList Списки слухачів
Vector/Hashtable НІКОЛИ (legacy)

Типові помилки

// 1. Забутий synchronized при ітерації
for (var e : syncList) { ... }  // CME!

// 2. Експорт оригіналу
List original = new ArrayList<>();
List sync = Collections.synchronizedList(original);
original.add("x");  // Thread-safety порушено!

// 3. Подвійна синхронізація
synchronized (syncList) {
    syncList.add(e);  // Внутрішній + зовнішній synchronized
}

🔴 Senior Level

Mutex object

// За замовчуванням mutex (mutual exclusion — об'єкт-ключ для synchronized) = this (обгортка)
// → Усі synchronized методи блокують один одного
// backing collection — оригінальна колекція, яку обгортає synchronized-обгортка

// Можна (через рефлексію) встановити свій mutex
// → Але JDK API не підтримує напряму

Production Experience

Чому ConcurrentHashMap краще:

synchronizedMap:
  → 1 лок на все
  → Читання блокує читання

ConcurrentHashMap:
  → Lock-free читання
  → CAS для запису
  → Гранулярні локи на корзину

Best Practices

  1. Ітерація → synchronized блок
  2. Не експортуйте оригінал
  3. ConcurrentHashMap переважніше
  4. mutex = this за замовчуванням
  5. Уникайте подвійної синхронізації

Резюме для Senior

  • Decorator патерн → synchronized на кожен метод
  • Ітерація → ручний synchronized блок
  • mutex = this → повне блокування
  • ConcurrentHashMap > synchronizedMap
  • Не експортуйте backing collection (оригінал, який обгортається)

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Collections.synchronizedList/Set/Map — фабричні методи-обгортки (Decorator патерн)
  • Кожна синхронізується на одному mutex об’єкті (за замовчуванням this)
  • Ітерація обов’язково вимагає synchronized(list) { ... } блока
  • Оригінальна колекція залишається доступною — зміна через неї порушує thread-safety
  • Подвійна синхронізація (synchronized(syncList) { syncList.add(e); }) — зайвий overhead
  • Альтернативи: ConcurrentHashMap (highload), CopyOnWriteArrayList (списки слухачів)
  • Vector/Hashtable — legacy, НІКОЛИ в новому коді

Часті уточнюючі запитання:

  • Що таке mutex в synchronized колекціях? — Об’єкт-ключ для synchronized; за замовчуванням this, всі методи блокують один одного.
  • Чому ітерація по synchronizedList без synchronized блока призводить до CME? — Ітератор викликає hasNext/next окремо; між ними інший потік може змінити колекцію.
  • Чим Collections.synchronizedList відрізняється від ConcurrentHashMap? — Перший — один лок на все; другий — lock-free читання + гранулярні локи.
  • Що таке backing collection? — Оригінальна колекція, яку обгортає synchronized-обгортка; доступ до неї в обхід обгортки ламає thread-safety.

Червоні прапорці (НЕ говорити):

  • «Можна змінювати оригінальну колекцію без наслідків» — це порушує thread-safety
  • «synchronizedList гарантує атомарність складових операцій» — ні, потрібен зовнішній synchronized
  • «Iterator.remove() працює без synchronized для synchronizedList» — ітерація завжди вимагає ручного блокування
  • «Vector — сучасна альтернатива» — це legacy з грубим блокуванням

Пов’язані теми:

  • [[21. Коли використовувати synchronized collections]]
  • [[23. Що таке Collections.unmodifiableList()]]
  • [[25. В чому різниця між Iterator та ListIterator]]