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