Копировать при записи (COW) в Java

Java задняя часть Безопасность

Background

Копирование при записи (COW) иногда называют «неявным совместным использованием», как следует из названия, которое позволяет всем пользователям, которым необходимо использовать ресурс R, совместно использовать одну и ту же копию ресурса R, когда один из пользователей хочет использовать ресурс R. операцию модификации, сначала копируют копию R' из R, а затем выполняют операцию модификации;

Problem

В Java Collections Framework что-то вродеArrayList, HashSetТакие базовые классы коллекций не являются потокобезопасными, и в многопоточной среде могут выполняться одновременные операции обхода и модификации.ConcurrentModificationException; Для решения этой проблемы можно выполнить синхронизацию каждой операции, но синхронизация коллекции, в которой большинство операций считывают данные, может резко снизить производительность, и в этом случае снижение производительности не требуется;

Например, в следующих координатах ось X представляет ось времени, а ось Y представляет разные потоки.+представляет операцию чтения,*Представляет операцию модификации:

| ++ ++ ++   ++ ++ ++ ++
|++ + ++ +  ++ + +++ + +
|  ++ ++ *    ++ +* ++ +
| ++ + +   * ++ +  ++ + 
|+ + +++ *  + + +++ + ++
| + + +      + + + + + +
+----------------------
         1 2      3

За исключением трех моментов времени 1 2 3, остальные моменты времени являются только операциями чтения, и нет необходимости в синхронизации в моменты времени, отличные от 1 2 3;

Поэтому мы хотим использовать технику для работы с ресурсами, которые имеют гораздо больше операций чтения, чем операций модификации в многопоточной среде, чтобы не было необходимости синхронизировать каждую операцию;

Solution

Библиотека классов Java предоставляет два класса Copy-on-Write:CopyOnWriteArrayListиCopyOnWriteArraySet, соответственно, достигнутоListиSetдва интерфейса;

How CopyOnWriteArrayList Works

CopyOnWriteArrayListСинхронизация выполняется только при его изменении, поэтому егоadd, removeмеханизмы синхронизации используются в таких методах, какCopyOnWriteArrayList, определяет повторную блокировку:

final transient ReentrantLock lock = new ReentrantLock();

Эта блокировка используется во всех методах, которые изменяют коллекцию (add, removeи т. д.) для синхронизации, когда выполняется фактическая операция модификации, исходный массив будет сначала скопирован, затем изменен и, наконец, заменен оригиналом:

public boolean add(E e) {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		Object[] elements = getArray();
		int len = elements.length;
		Object[] newElements = Arrays.copyOf(elements, len + 1);
		newElements[len] = e;
		setArray(newElements);
		return true;
	} finally {
		lock.unlock();
	}
}

Поскольку копия данных копируется при их изменении, все чтения не нужно синхронизировать:

public E get(int index) {
	return get(getArray(), index);
}

Но это также приведет к проблеме «слабой согласованности»; так называемая «слабая согласованность» означает, что когда поток читает данные, если другой поток одновременно изменяет данные в этой области, читающий поток не будет способен читать последние данные, то есть поток чтения может читать только последние данные до времени чтения;

Другое проявление «слабой согласованности» заключается в том, что при использовании итератора, при использовании итератора для обхода коллекции, итератор может перейти только к данным, когда итератор был создан, а для модификации коллекции после создания итератора, итератор не может воспринимать; это потому, что когда итератор создается, итератор создает «моментальный снимок» исходных данных; поэтомуCopyOnWriteArrayListиCopyOnWriteArraySetЕго можно применять только к сценариям, не требующим высоких данных в реальном времени;

How CopyOnWriteArraySet Works

CopyOnWriteArraySetреализация основана наCopyOnWriteArrayList, который внутренне поддерживаетCopyOnWriteArrayListПримерal:

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
	al = new CopyOnWriteArrayList<E>();
}

все парыCopyOnWriteArraySetоперации делегируютсяal, какaddметод:

public boolean add(E e) {
	return al.addIfAbsent(e);
}

очень типичноКомбинированный режимприменение;