CopyOnWriteArrayList параллельных контейнеров

Java задняя часть Debug Безопасность
CopyOnWriteArrayList параллельных контейнеров

Оригинальная статья, краткое изложение опыта и жизненные перипетии на всем пути от набора в школу до фабрики.

Нажмите, чтобы узнать подробностиwww.codercc.com

1. Введение в CopyOnWriteArrayList

Все изучающие Java знают, что ArrayList не является потокобезопасным.Когда читающий поток читает ArrayList, если есть записывающий поток, записывающий данные, на основе механизма быстрого сбоя, он выдастConcurrentModificationExceptionException, то есть ArrayList не является потокобезопасным контейнером.Конечно, вы можете использовать Vector или использовать статический метод Collections, чтобы обернуть ArrayList в потокобезопасный класс, но эти методы используют ключевое слово java, синхронизированное с изменить метод, используя эксклюзивные блокировки для обеспечения безопасности потоков. Однако, поскольку только один поток монопольной блокировки может одновременно получить монитор объекта, очевидно, что этот метод не очень эффективен.

Вернемся к бизнес-сценарию, есть много предприятий, которые больше читают и меньше пишут, например, информацию о конфигурации системы, за исключением того, что данные необходимо записывать во время начальной конфигурации системы, другим модулям нужно только считывать системную информацию по истечении большей части времени. , Например, белый список, черный список и другие конфигурации, вам нужно только прочитать конфигурацию списка и проверить, находится ли текущий пользователь в пределах диапазона конфигурации. Подобных бизнес-сценариев много, все они относятся кБольше читайте и меньше пишитеместо действия. Если в данном случае используются вышеперечисленные методы, то нецелесообразно использовать методы преобразования Вектора, Коллекции, поскольку, хотя несколько потоков чтения считывают данные из одного и того же контейнера данных, потоки чтения не затрагивают данные контейнера данных. Будут модификации. Естественно, мы будем думать о ReenTrantReadWriteLock (о блокировках чтения-записи см.эта статья),пройти черезразделение чтения-записиИдея заключается в том, что не будет блокировки между чтением и чтением.Несомненно, если список может быть прочитан несколькими потоками чтения, производительность значительно улучшится. Однако, если список снова инкапсулирован только блокировкой чтения-записи (ReentrantReadWriteLock), из-за характеристик блокировки чтения-записи, когда блокировка записи получена потоком записи, поток чтения и записи будет заблокирован. Если мы используем только блокировки чтения-записи для инкапсуляции списка, все равно возникает ситуация, когда читающий поток блокируется при чтении данных.Если мы хотим, чтобы эффективность чтения списка была выше, это наш прорыв.Если мы гарантируем, что поток чтения будет заблокирован всякий раз, когда они не заблокированы, не будет ли эффективность выше?

Мастер Дуг Ли предоставил нам контейнер CopyOnWriteArrayList для обеспечения безопасности потоков и гарантии того, что чтение не будет заблокировано в любое время. CopyOnWriteArrayList также широко используется во многих бизнес-сценариях. С CopyOnWriteArrayList стоит познакомиться.

2. Дизайн-мышление COW

Возвращаясь к упомянутому выше, если вы используете блокировку чтения и записи, после получения блокировки записи поток чтения и записи блокируется, и только когда блокировка записи снимается, поток чтения будет иметь возможность получить блокировку для читать последние данные.С точки зрения потока чтения, то есть поток чтения получает самые последние данные в любое время, что соответствует характеру данных в реальном времени.. Поскольку мы говорим об оптимизации, должен быть компромисс, мы можемДостаточно пожертвовать данными в реальном времени, чтобы обеспечить возможную согласованность данных.. CopyOnWriteArrayList использует Copy-On-Write (COW), то есть идею копирования при записи, для достижения окончательной согласованности данных посредством стратегии отложенного обновления и для обеспечения того, чтобы потоки чтения не блокировались.

Популярное понимание COW заключается в том, что когда мы добавляем элементы в контейнер, мы не добавляем напрямую в текущий контейнер, а сначала копируем текущий контейнер, копируем новый контейнер, а затем добавляем элементы в новый контейнер после добавления элементов. , а затем укажите ссылку исходного контейнера на новый контейнер. При одновременном чтении контейнера CopyOnWrite нет необходимости в блокировке, поскольку текущий контейнер не будет добавлять никаких элементов. Таким образом, контейнер CopyOnWrite также является идеей разделения чтения и записи.Стратегия отложенного обновления реализуется путем нацеливания на разные контейнеры данных при записи, отказа от данных в реальном времени для достижения окончательной согласованности данных.

3. Принцип реализации CopyOnWriteArrayList

Теперь давайте разберемся с CopyOnWriteArrayList, взглянув на исходный код. Фактически, CopyOnWriteArrayList внутренне поддерживает массив

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

И ссылка на массив изменена volatile, обратите внимание здесьЭто просто ссылка на массив, есть еще одна тайна, которая будет раскрыта позже. Очень важным свойством volatile является то, что он может гарантировать видимость Подробное объяснение volatile см.эта статья. Для списка нас, естественно, больше всего беспокоит реализация методов get и add при чтении и записи.

3.1 Принцип реализации метода get

Исходный код метода get:

public E get(int index) {
    return get(getArray(), index);
}
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

Видно, что реализация метода get очень проста, это почти «однопоточная» программа, она не добавляет к многопоточности никакого контроля безопасности потоков, не имеет блокировок, CAS-операций и т. д. Причина в том, что все потоки чтения могут только читать. Данные в контейнере данных извлекаются и не будут изменены.

3.2 Принцип реализации метода add

Давайте посмотрим, как добавить данные? Исходный код метода добавления:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
	//1. 使用Lock,保证写线程在同一时刻只有一个
    lock.lock();
    try {
		//2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;
		//3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
		//4. 往新数组中添加新的数据
		newElements[len] = e;
		//5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

Логику метода add также легче понять, см. комментарии выше. Несколько замечаний:

  1. Используйте ReentrantLock, чтобы убедиться, что только один поток записи одновременно копирует массив, иначе в памяти будет несколько копий скопированных данных;
  2. Как упоминалось ранее, ссылка на массив изменчива, поэтому старая ссылка на массив указывает на новый массив.Согласно правилу volatile, модификация ссылки на массив записывающим потоком видна читающему потоку.
  3. Поскольку данные вставляются в новый массив при записи данных, гарантируется, что операции чтения и записи выполняются в двух разных контейнерах данных.

4. Резюме

Мы знаем, что и блокировка COW, и блокировка чтения-записи реализованы с помощью идеи разделения чтения-записи, но они все же несколько отличаются и их можно сравнивать:

COW против блокировок чтения-записи

Сходства: 1. Оба реализованы через идею разделения чтения и записи 2. Потоки чтения не блокируют друг друга

разница:Для потока чтения, чтобы получить данные в реальном времени, поток чтения будет ждать после получения блокировки записи или поток записи будет ждать после получения блокировки чтения, тем самым решая проблему «грязного чтения». То есть, если используется блокировка чтения-записи, поток чтения все равно будет блокироваться и ждать. COW, с другой стороны, полностью избавляет от необходимости жертвовать данными в реальном времени, чтобы обеспечить окончательную согласованность данных, то есть обновление данных потоком чтения происходит с учетом задержки, поэтому поток чтения не должен ждать..

Из текста это пока сложно понять, давайте пробежимся по отладке, основной код метода add:

1.Object[] elements = getArray();
2.int len = elements.length;
3.Object[] newElements = Arrays.copyOf(elements, len + 1);
4.newElements[len] = e;
5.setArray(newElements);

Предположим, что изменение COW показано на следующем рисунке:

最终一致性的分析.png
Анализ конечной согласованности.png

В массиве уже есть данные 1, 2 и 3. Теперь поток записи хочет добавить в массив данные 4. Мы устанавливаем точку останова в строке 5, чтобы приостановить поток записи. Поток чтения по-прежнему сможет читать данные из массива «незатронутыми», но он по-прежнему сможет читать только 1,2,3.Если поток чтения может немедленно прочитать вновь добавленные данные, это называется обеспечением данных в реальном времени.. Когда точка останова в строке 5 освобождается, поток чтения может воспринять изменение данных и прочитать полные данные 1, 2, 3, 4 и гарантироватьВозможная согласованность данных, хотя это может быть воспринято с промежутком в несколько секунд.

Вот еще вопрос:Зачем нужно копировать? Если массив массива установлен как volatile, запись происходит до чтения в volatile переменную, читающий поток не в состоянии воспринять изменение volatile переменной.

Причина в том, что здесь volatile ModifiedТолькоТолькоссылка на массив,Модификация элементов в массиве не гарантирует видимости. Поэтому COW использует два контейнера данных, старый и новый, и указывает ссылку на новый массив через 5-ю строку кода.

Это также является причиной того, что concurrentHashMap имеет только слабую согласованность, слабая согласованность concurrentHashMap может бытьсм. эту статью.

Недостатки КОРОВ

Контейнер CopyOnWrite имеет много преимуществ, но у него также есть две проблемы, а именно использование памяти и согласованность данных. Так что вам нужно обратить внимание при разработке.

  1. проблема с использованием памяти: Из-за механизма копирования при записи CopyOnWrite, когда выполняется операция записи, память двух объектов будет находиться в памяти одновременно, старый объект и вновь записанный объект (Примечание: при копировании только ссылка в контейнере копируется, но при записи новые объекты будут создаваться и добавляться в новый контейнер, а объекты старого контейнера все еще используются, поэтому имеется две объектной памяти). Если память, занимаемая этими объектами, относительно велика, например, около 200 МБ, то при записи 100 МБ данных память будет занимать 300 МБ, и это время, вероятно, вызовет частый минорный GC и крупный GC.

  2. Проблемы согласованности данных: Контейнер CopyOnWrite может гарантировать только окончательную согласованность данных, но не согласованность данных в реальном времени. Поэтому, если вы хотите, чтобы записываемые данные считывались немедленно, не используйте контейнер CopyOnWrite.

использованная литература

  1. Искусство параллельного программирования на Java
  2. КОРОВОЕ объяснение