CopyOnWriteArrayList: примеры (JAVA)

Руководство по CopyOnWriteArrayList для Java
Раздел: Многопоточность, Коллекции
CopyOnWriteArrayList: class

Описание и поведение

Класс CopyOnWriteArrayList находится в пакете java.util.concurrent. Это потокобезопасная реализация списка с семантикой «копировать при записи»: все мутации (добавление, удаление, замена) выполняют копирование внутренних массивов, тогда как чтения обращаются к неизменяемой снимку массива без блокировок. Такая модель оптимальна для сценариев с частыми операциями чтения и редкими изменениями.

Класс реализует интерфейсы List<E>, RandomAccess, Cloneable, Serializable. Итератор возвращает моментальный снимок содержимого: он не отражает последующие модификации списка и не бросает ConcurrentModificationException. Итератор не поддерживает операцию remove.

Конструкторы, аргументы и возвращаемые значения

  • CopyOnWriteArrayList() - создаёт пустой список. Возвращаемого значения нет.
  • CopyOnWriteArrayList(Collection<? extends E> c) - создаёт список, скопировав элементы коллекции c. Если c равна null, выбрасывается NullPointerException.
  • CopyOnWriteArrayList(E[] toCopyIn) - создаёт список на основе массива. Копирование массива происходит при создании.

Основные методы и их поведение

  • boolean add(E e) - добавляет элемент в конец. Возвращает true (как у большинства коллекций). Выполняет копирование внутреннего массива.
  • void add(int index, E element) - вставка по индексу с копированием. При неверном индексе IndexOutOfBoundsException.
  • boolean addIfAbsent(E e) - добавляет элемент только если его ещё нет; операция атомарна по отношению к другим методам этого списка и возвращает true, если элемент был добавлен.
  • boolean addAll(Collection<? extends E> c) и int addAllAbsent(Collection<? extends E> c) - добавление коллекции с копированием; addAllAbsent добавляет только отсутствующие элементы и возвращает число добавленных.
  • E get(int index) - получение по индексу без блокировок; при неверном индексе IndexOutOfBoundsException.
  • E set(int index, E element) - замена по индексу; возвращает прежний элемент; выполняется копирование.
  • boolean remove(Object o) и E remove(int index) - удаление с копированием; возвращают успех или удалённый элемент.
  • int size(), boolean isEmpty(), boolean contains(Object o) - операции чтения без блокировок.
  • Iterator<E> iterator() - итератор над снимком в момент вызова; не отражает будущих изменений и не поддерживает remove.
  • Методы из Java 8+ (например, forEach, removeIf, replaceAll) доступны как реализация интерфейсов; при их использовании происходит копирование внутренней структуры при мутации.

Общее правило: операции чтения дешёвые и не блокирующие, операции записи создают новую копию внутреннего массива, что увеличивает расход памяти и стоимость при частых модификациях.

Короткие примеры использования

Примеры демонстрируют основные сценарии: создание, чтение, поведение итератора и специализированные методы.

1. Создание и обычные операции

import java.util.concurrent.CopyOnWriteArrayList;

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
String v = list.get(1);
int s = list.size();
v = "B"
s = 2

2. Итератор и снимок состояния

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(new String[]{"x","y"});
for (String t : list) {
    // внутри итерации добавляется новый элемент
    list.add("z");
    System.out.println(t);
}
System.out.println(list);
Вывод:
"x"
"y"
["x", "y", "z"]

3. addIfAbsent для семантики множества

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
boolean added = list.addIfAbsent("a");
added = false
list = ["a"]

4. Конструктор с коллекцией

List<Integer> src = Arrays.asList(1,2,3);
CopyOnWriteArrayList<Integer> cow = new CopyOnWriteArrayList<>(src);
cow = [1, 2, 3]

Похожие коллекции в Java и их особенности

  • Collections.synchronizedList(List) - синхронизированная оболочка вокруг List. Подходит при частых мутациях и при необходимости согласованности итератора (требует внешней синхронизации при итерации). Чтение и запись блокируют одну общую мьютекс.
  • CopyOnWriteArraySet - набор, основанный на CopyOnWriteArrayList. Подходит при необходимости множества с редкими изменениями и частыми чтениями.
  • Vector - устаревший синхронизированный список. Менее предпочтителен по сравнению с современными альтернативами.
  • ConcurrentLinkedQueue - неблокирующая очередь для высоконагруженных сценариев с частыми вставками и удалениями; не обеспечивает произвольного доступа по индексу.
  • ConcurrentHashMap / ConcurrentSkipListMap - структуры для карт и наборов с высокой конкурентностью. Предпочтительнее при частых обновлениях и большого числа потоков.

Выбор основывается на балансе чтений и записей, требованиях к консистентности итераторов и необходимости индексации.

Аналоги в других языках и отличия

PHP

Массивы PHP реализованы с семантикой copy-on-write на уровне движка; копирование выполняется при модификации. Однако это не даёт потокобезопасности в многопоточном окружении и не предоставляет специфичных методов, как addIfAbsent.

// PHP-псевдокод
$a = [1,2];
$b = $a; // копирование по ссылке, физическое копирование при записи
$b[] = 3; // теперь создана копия
$a = [1,2]
$b = [1,2,3]

JavaScript

В среде Node.js и браузерах однопоточные структуры делают CopyOnWrite редко нужным. Библиотеки вроде Immutable.js предоставляют неизменяемые коллекции с структурным шарингом, что экономит память по сравнению с полным копированием.

// JS с Immutable.js
const { List } = require('immutable');
let l1 = List([1,2]);
let l2 = l1.push(3);
l1 = List [1,2]
l2 = List [1,2,3]

Python

Стандартный list не потокобезопасен. Для неизменяемости используется tuple или копирование списка вручную (list.copy()). Для конкурентного доступа применяется threading.RLock или структуры из queue и сторонние immutable-библиотеки.

# Python
lst = [1,2]
lst2 = lst.copy()
lst2.append(3)
lst = [1,2]
lst2 = [1,2,3]

C#

В .NET есть System.Collections.Concurrent и пакет System.Collections.Immutable. ImmutableList обеспечивает неизменяемость с эффективным структурным шарингом; для Copy-On-Write семантики в .NET обычно применяется ImmutableList или ручное копирование List<T>.

// C#
var list = ImmutableList.Create(1,2);
var list2 = list.Add(3);
list = [1,2]
list2 = [1,2,3]

Go (Golang)

Слайсы в Go копируются при присваивании ссылочно; для паттерна copy-on-write требуется явное копирование с использованием append или копирования с помощью copy(). Для потокобезопасности применяется sync.RWMutex или атомарные операции и immutable-структуры.

// Go
s := []int{1,2}
s2 := append([]int(nil), s...)
s2 = append(s2, 3)
s = [1,2]
s2 = [1,2,3]

Kotlin

Kotlin использует Java-коллекции и предоставляет неизменяемые виды через List и MutableList. CopyOnWriteArrayList доступен напрямую из JVM и ведёт себя так же, как в Java.

Lua

Таблицы Lua не имеют встроенной потоковой безопасности. Для copy-on-write требуется ручное копирование таблицы при изменении и внешняя синхронизация при использовании в многопоточном окружении (корутины обычно не используют параллелизм в стандартной реализации).

В заключение, в большинстве языков есть либо внутренний copy-on-write (как в PHP для массивов) или функции для создания неизменяемых/иммутабельных коллекций; отличия касаются многопоточности, стоимости копирования и наличия атомарных методов типа addIfAbsent.

Типичные ошибки и заблуждения

  • Ожидание, что итератор будет отражать последующие изменения. На деле итератор работает со снимком; изменения, выполненные после создания итератора, не будут видны. Пример:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(Arrays.asList("a","b"));
Iterator<String> it = list.iterator();
list.add("c");
while (it.hasNext()) {
    System.out.println(it.next());
}
Выведет:
a
b
// "c" не будет выведен итератором
  • Использование в сценариях с частыми записями. Полное копирование при каждой мутации приводит к высоким накладным расходам по времени и памяти. Предпочтительнее выбирать конкурентные коллекции с более дешёвыми мутациями при высокой интенсивности записи.
  • Попытка вызывать remove на итераторе. Итератор снимка не поддерживает модификации через себя - будет брошено UnsupportedOperationException.
  • Недооценка расхода памяти при больших списках: каждая мутация создаёт новую копию массива, пока прежние копии не будут собраны сборщиком мусора.
  • Ожидание атомарности комплексных операций. Хотя отдельные методы, такие как addIfAbsent, атомарны, комбинации операций (например, проверка и затем вставка несколькими вызовами) не являются атомарными без внешней синхронизации.

Изменения и история развития

Класс CopyOnWriteArrayList введён в Java 5 как часть пакета java.util.concurrent. В Java 8 и более поздних версиях появились методы потокового API и default-методы в коллекциях (forEach, removeIf, replaceAll и т.д.), которые могут использоваться с CopyOnWriteArrayList; они сохраняют семантику копирования при мутациях. Специфичных кардинальных изменений в самой реализации класса в последних версиях не было, важные изменения касались совместимости с новыми API коллекций и улучшений производительности JVM в части копирования и сборки мусора.

Расширенные примеры и нестандартные сценарии

1. Менеджер слушателей (listeners) в многопоточной среде

Пример java
import java.util.concurrent.CopyOnWriteArrayList;

public class EventBus {
    private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();

    public void register(Runnable r) { listeners.add(r); }
    public void unregister(Runnable r) { listeners.remove(r); }
    public void fire() {
        for (Runnable r : listeners) {
            r.run(); // нет внешней синхронизации, безопасно для чтения
        }
    }
}
Поведение: несколько потоков могут регистрировать/удалять слушателей, а вызов fire() безопасно переберет снимок слушателей на момент вызова.

2. Использование addIfAbsent для реализации множеств с атомарной вставкой

Пример java
CopyOnWriteArrayList<String> setLike = new CopyOnWriteArrayList<>();
// параллельный поток A
setLike.addIfAbsent("k");
// параллельный поток B
setLike.addIfAbsent("k");
После конкурентных вызовов элемент "k" будет добавлен не более одного раза благодаря атомарности addIfAbsent.

3. Сравнение накладных расходов: простая микро-оценка

Пример java
// Псевдокод для измерения времени добавления
CopyOnWriteArrayList<Integer> cow = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100_000; i++) cow.add(i);
// vs
List<Integer> sync = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100_000; i++) sync.add(i);
Результат зависит от нагрузки и числа потоков: при одиночном потоке ArrayList быстрее; при многих потоках и в сценариях чтений CopyOnWriteArrayList может показать лучшие показатели за счёт отсутствия блокировок на чтение.

4. Комбинация с volatile и ссылочной семантикой

Иногда хранение ссылки на CopyOnWriteArrayList в volatile-поле позволяет быстро менять всю структуру списка целиком и гарантировать видимость обновления всем потокам.

Пример java
volatile CopyOnWriteArrayList<String> ref = new CopyOnWriteArrayList<>();
// обновление полной версии
CopyOnWriteArrayList<String> newVer = new CopyOnWriteArrayList<>(ref);
newVer.add("extra");
ref = newVer; // атомарная смена ссылки
Другие потоки после присваивания увидят новую версию целиком без необходимости синхронизации.

5. Использование с параллельными потоками и стримами

Пример java
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(Arrays.asList(1,2,3,4));
list.parallelStream().forEach(System.out::println);
Вывод элементов с возможным произвольным порядком. Параллельный стрим не модифицирует исходный список, и при модификациях во время стрима поведение определяется снимком данных.

6. Нетипичный сценарий: частичная массовая замена

Для замены большого количества элементов предпочтительнее собрать новую коллекцию и подставить её целиком (через присваивание ссылки или создание нового экземпляра), чтобы минимизировать число копирований.

Пример java
CopyOnWriteArrayList<String> current = new CopyOnWriteArrayList<>(old);
List<String> modified = current.stream()
    .map(s -> transform(s))
    .collect(Collectors.toList());
CopyOnWriteArrayList<String> replaced = new CopyOnWriteArrayList<>(modified);
current = replaced; // переключить ссылку atomically если поле volatile
Результат: единственное создание копии вместо множества промежуточных копий при последовательных set/add.

В целом CopyOnWriteArrayList хорош в паттернах «много чтений, редкие записи», для реализации менеджеров слушателей и для упрощения логики конкурентного доступа, когда стоимость копирования остаётся приемлемой.

джава CopyOnWriteArrayList function comments

En
CopyOnWriteArrayList Потокобезопасный ArrayList с копированием при записи