Что такое 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
- Memory: при записи временно удваивается память (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 и какие реализации существуют]]