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

Що таке CopyOnWriteArrayList?

4. Iterator = snapshot, не live view 5. Memory spike при записі → ризик OOM 6. Stale data — припустимо для вашого сценарію? 7. Альтернативи: ConcurrentLinkedQueue, synchronizedList

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

🟢 Junior Level

CopyOnWriteArrayList — потокобезпечний список, який копіює масив при кожному записі.

Проста аналогія: Фотографія. Читачі дивляться на фото (стару версію), а автор робить нову фотографію (копію зі змінами).

List<String> list = new CopyOnWriteArrayList<>();

// Читання: швидко, без блокувань.
// volatile Object[] array → get() = просте читання з масиву, без атомарних операцій і synchronized.
String s = list.get(0);

// Запис: створює копію масиву
list.add("A");  // Копія → додавання → заміна

Коли використовувати:

  • Багато читань, мало записів
  • Списки слухачів (listeners)

🟡 Middle Level

Як працює

// Всередині: volatile Object[] array

// Читання (get):
return array[index];  // Без блокувань!

// Запис (add):
1. Lock
2. newArray = Arrays.copyOf(array, size + 1)
3. newArray[size] = element
4. array = newArray  // volatile запис
5. Unlock

Iterator = Snapshot

List<String> list = new CopyOnWriteArrayList<>();
list.add("A");

Iterator<String> it = list.iterator();  // Snapshot!
list.add("B");  // Змінили список

it.next();  // → "A" (ітератор НЕ бачить "B"!)
// → Fail-Safe, ніколи не кидає Exception

// it.remove() → UnsupportedOperationException!

Коли використовувати

// ✅ Списки слухачів
List<Listener> listeners = new CopyOnWriteArrayList<>();

// Читання (сповіщення) >>> записів (підписка)
for (Listener l : listeners) {  // Швидко!
    l.onEvent();
}

// ✅ Рідко змінювані кеші

Коли НЕ використовувати

// ❌ Часті записи
list.add(e);  // Копія масиву щоразу!
// → O(n) на запис
// → Подвоєння пам'яті тимчасово

// ✅ Альтернативи:
ConcurrentLinkedQueue<String> queue;
Collections.synchronizedList(list);

**Ключова відмінність:** synchronizedList блокує кожне читання, COWAL не блокує. synchronizedList ітератор кидає CME при модифікації, COWAL  ні (snapshot).

🔴 Senior Level

Memory Footprint

// При записі:
oldArray: 1 млн елементів  4 МБ
newArray: 1 млн + 1  4 МБ
// → 8 МБ тимчасово!

// Для великих списків → ризик OOM
// → GC тиск при частих записах

Happens-Before гарантія

// volatile array = Happens-Before
// Запис в одному потоці → видна всім в інших

// Але: вікно між копіюванням і записом
// → Читачі можуть бачити «старі» дані

Stale Data проблема

  • Fail-Safe = ітератор ніколи не кидає ConcurrentModificationException (працює з копією)
  • Stale Data = «застарілі дані» — читач бачить версію масиву на момент створення ітератора
  • Eventual Consistency = дані стануть актуальними не миттєво, а після наступного запису
// Потік 1: читає array (стара версія)
// Потік 2: копіює, модифікує, записує новий array
// Потік 1: все ще читає старий array

 Консистентність eventual, не strong!

Iterator обмеження

// Fail-Safe:
// → Ніколи ConcurrentModificationException
// → Але: не бачить зміни після створення

// Методи ітератора:
it.remove()  // → UnsupportedOperationException!
it.add()     // → UnsupportedOperationException!
it.set()     // → UnsupportedOperationException!

Production Experience

Реальний сценарій: Listener list

// Event Bus: 1000 підписників
// Сповіщення: 100,000/сек
// Підписка/відписка: 1/сек

// Співвідношення 100,000 читань до 1 запису → копіювання масиву (O(n)) відбувається 1 раз на 100,000 операцій → оверхед незначний.

CopyOnWriteArrayList:
   Читання: O(1), lock-free
   Запис: O(n), але рідко!
   Ідеально підходить!

Best Practices

  1. Переважно для read-heavy сценаріїв. Припустимо для маленьких колекцій (< 100) навіть при помірних записах.
  2. Списки слухачів — ідеальне застосування
  3. Уникайте при частих записах
  4. Iterator = snapshot, не live view
  5. Memory spike при записі → ризик OOM
  6. Stale data — припустимо для вашого сценарію?
  7. Альтернативи: ConcurrentLinkedQueue, synchronizedList

Резюме для Senior

  • Copy-On-Write = копія масиву при записі
  • Читання = O(1), lock-free, volatile array
  • Запис = O(n), копіювання + заміна
  • Iterator = snapshot, fail-safe
  • Пам’ять = подвоєння тимчасово при записі
  • Listeners = ідеальне застосування
  • Stale data = eventual consistency
  • Уникайте для write-heavy сценаріїв

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

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

  • CopyOnWriteArrayList — при кожному записі (add/set/remove) створює копію внутрішнього масиву, читання lock-free через volatile
  • Читання = O(1), без блокувань. Запис = O(n), копіювання + заміна масиву + lock
  • Iterator = snapshot на момент створення, fail-safe (не кидає CME), не бачить зміни після створення
  • Iterator НЕ підтримує remove/add/set — UnsupportedOperationException
  • Ідеальне застосування: списки слухачів (listeners) — ratio читань до записів 100,000:1
  • Пам’ять: при записі тимчасово подвоюється пам’ять (oldArray + newArray) — ризик OOM для великих списків
  • Happens-Before гарантія через volatile array, але eventual consistency — читачі можуть бачити старі дані
  • Альтернативи для write-heavy: ConcurrentLinkedQueue, Collections.synchronizedList

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

  • Чому iterator не бачить додані елементи? — Iterator працює зі snapshot (копією масиву на момент створення). Нові елементи додаються в новий масив — snapshot їх не бачить.
  • Чим COWAL відрізняється від synchronizedList? — synchronizedList блокує КОЖНЕ читання. COWAL не блокує читання. synchronizedList iterator кидає CME при модифікації, COWAL — ні (snapshot).
  • Коли COWAL — поганий вибір? — При частих записах: кожен запис = копіювання всього масиву. Для 1 млн елементів = 4 МБ копіювання + тимчасове подвоєння пам’яті.
  • Що таке eventual consistency в COWAL? — Потік читає старий масив, інший потік записує новий. Читач побачить нові дані не миттєво, а тільки після наступного посилання на array.

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

  • ❌ «COWAL блокує читання» — ні, читання повністю lock-free через volatile
  • ❌ «Iterator COWAL показує live дані» — це snapshot, не бачить зміни після створення
  • ❌ «COWAL підходить для частих записів» — O(n) на запис + подвоєння пам’яті, використовуйте ConcurrentLinkedQueue
  • ❌ «COWAL iterator підтримує remove()» — ні, UnsupportedOperationException для всіх модифікацій

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

  • [[18. Що таке ConcurrentHashMap]]
  • [[19. Як ConcurrentHashMap забезпечує thread-safety]]
  • [[14. Що таке Map і які реалізації існують]]