предисловие
Только лысина может стать сильнее
Некоторое время назад я написал статью COW (Copy On Write), но объем чтения был очень низким...КОРОВЫЕ коровы! Узнайте о механизме копирования при записи
Может быть, все знакомы с этой технологиейотносительно незнакомыйНу, а эта технологияМножество сценариев примененияиз. В дополнение к Linux и файловым системам, упомянутым выше, на самом делеJavaУ него тоже есть свой образ.
Вероятно, наиболее знакомым потокобезопасным контейнером является ConcurrentHashMap, потому что этот контейнер часто исследуют во время собеседований.
Например, стандартный сценарий интервью:
- Интервьюер спросил: «Является ли HashMap потокобезопасным? Если HashMap не является потокобезопасным, существует ли безопасный контейнер Map?»
- 3y: «Есть две потокобезопасные карты: одна — Hashtable, а другая — ConcurrentHashMap».
- Интервьюер продолжал спрашивать: «В чем разница между Hashtable и ConcurrentHashMap?»
- 3г: "балабалабалабалабалабала"
- Интервьюер: «Хорошо, хорошо, хорошо, ваша база Java довольно хороша»
А если бы было такое интервью?
- Интервьюер спросил: «Является ли ArrayList потокобезопасным? Если ArrayList не является потокобезопасным, существует ли безопасный контейнер, подобный ArrayList?»
- 3y: «Мы можем использовать Vector для потокобезопасного ArrayList, или мы можем использовать методы из Collections, чтобы обернуть его»
- Интервьюер продолжал спрашивать: «Ну, я думаю, вы также знаете, что Vector — это старый контейнер, есть что-нибудь еще?»
- 3г: "Эмммм, это..."
- интервьюернамекать: "Например, в JUC есть ConcurrentHashMap, есть ли в JUC потокобезопасный контейнерный класс, такой как "ArrayList"?"
- 3г: "Эмммм, это..."
- Интервьюер: «Хорошо, хорошо, хорошо,Сегодня почти время для интервью, возвращайтесь и ждите уведомления. "
Сегодняшнее главное объяснение — CopyOnWriteArrayList~
эта статьяСтремитесь объяснить каждый пункт знаний просто, я надеюсь, что каждый сможет что-то получить после прочтения
1. Вектор и синхронизированный список
1.1 Обзор потокобезопасных векторов и SynchronizedList
Мы знаем, что ArrayList используется для замены Vector, который является потокобезопасным контейнером. потому что он добавляет почти каждое объявление методасинхронизированное ключевое словосделать контейнер безопасным.
При использованииCollections.synchronizedList(new ArrayList())
Чтобы сделать ArrayList потокобезопасным, ключевое слово synchronized добавляется почти в каждый метод, ноДобавляется не при объявлении метода, а внутри метода.
1.2 Возможные проблемы с Vector и SynchronizedList
Прежде чем объяснять контейнер CopyOnWrite, давайте взглянем на некоторые потокобезопасные контейнеры.может не заметилместо~
Давайте посмотрим на этот код напрямую:
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
Давайте проанализируем два вышеупомянутых метода с нашей первой реакцией:Есть ли проблема в многопоточной среде?
- Все, что мы можем знать, это то, что Вектор
size()和get()以及remove()
Все модифицируются синхронизированными.
Ответ: с точки зрения вызывающего абонента даесть проблемаиз
Мы можем написать кусок кода для проверки:
import java.util.Vector;
public class UnsafeVectorHelpers {
public static void main(String[] args) {
// 初始化Vector
Vector<String> vector = new Vector();
vector.add("关注公众号");
vector.add("Java3y");
vector.add("买Linux可到我下面的链接,享受最低价");
vector.add("给3y加鸡腿");
new Thread(() -> getLast(vector)).start();
new Thread(() -> deleteLast(vector)).start();
new Thread(() -> getLast(vector)).start();
new Thread(() -> deleteLast(vector)).start();
}
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
Можно обнаружить, что может быть выдано исключение:
Причина также очень проста, мы просто следуем процессу:
- Поток A выполняется
getLast()
метод, поток B выполняетdeleteLast()
метод - Поток A выполняется
int lastIndex = list.size() - 1;
Получите значение lastIndex равное 3.в то же время, поток B выполняетint lastIndex = list.size() - 1;
Получить значение lastIndexтакжеэто 3 - В этот момент поток B сначала получает право на выполнение CPU и выполняет
list.remove(lastIndex)
Элемент с индексом 3 удаляется - Затем поток A получает право на выполнение ЦП и выполняет
list.get(lastIndex);
, обнаруживается, что элемента с индексом 3 нет, и генерируется исключение.
Причина этой проблемы также очень проста:
-
getLast()
иdeleteLast()
Эти два метода не являются атомарными, хотя ониКаждая операция внутри атомарна(его можно изменить с помощью Synchronize для достижения атомарности), но все же возможнопопеременновоплощать в жизнь.- Здесь это означает:
size()和get()以及remove()
являются атомарными, но если выполняются одновременноgetLast()
иdeleteLast()
, внутри методаsize()和get()以及remove()
можно выполнять поочередно.
- Здесь это означает:
Решить вышеописанную ситуацию тоже очень просто, ведь мы все работаем на Векторе,Пока вы блокируете Вектор, прежде чем манипулировать им, все будет в порядке.!
Таким образом, мы можем изменить его на это:
// 得到Vector最后一个元素
public static Object getLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
// 删除Vector最后一个元素
public static void deleteLast(Vector list) {
synchronized (list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
ps: если кто-то проверит его, он выдаст исключение java.lang.ArrayIndexOutOfBoundsException: -1, чтоИсключение не проверяет углы, а не проблема, вызванная параллелизмом.
После приведенного выше примера мы можем посмотреть на следующий код:
public static void main(String[] args) {
// 初始化Vector
Vector<String> vector = new Vector();
vector.add("关注公众号");
vector.add("Java3y");
vector.add("买Linux可到我下面的链接,享受最低价");
vector.add("给3y加鸡腿");
// 遍历Vector
for (int i = 0; i < vector.size(); i++) {
// 比如在这执行vector.clear();
//new Thread(() -> vector.clear()).start();
System.out.println(vector.get(i));
}
}
Точно так же: если другой поток изменит длину вектора при обходе вектора, он все равно будетесть проблема!
- Поток A пересекает вектор и выполняет
vector.size()
Когда найдено, что вектор имеет длину 5 - В настоящее времявероятно, существуетПоток B выполняет вектор
clear()
действовать - Затем поток A выполняет
vector.get(i)
когда выбрасывается исключение
После JDK5 рекомендуется использовать Javafor-each
(итератор) для обхода нашей коллекции, преимуществоКраткие, проиндексированные массивом граничные значения вычисляются только один раз.
При использованииfor-each
(Итератор), чтобы выполнить вышеуказанную операцию, он вызовет исключение ConcurrentModificationException.
SynchronizedList используетобход итератораТакже будут проблемы, когда исходный код напомнил нам о ручной блокировке.
Если мы хотим идеально решить вышеуказанные проблемы, мы можемблокировка перед обходом:
// 遍历Vector
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
Опытные студенты могут знать:Вау, мне нужно добавить блокировку для обхода контейнера. Разве это не медленно до смерти?.Это очень медленно ..
Итак, наш CopyOnWriteArrayList готов!
2. Введение в CopyOnWriteArrayList(Set)
В общем, будем думать: CopyOnWriteArrayList — это замена синхронизированному List, а CopyOnWriteArraySet — замена синхронизированному Set.
Будь то Hashtable-->ConcurrentHashMap или Vector-->CopyOnWriteArrayList. По сравнению с потокобезопасным классом старого поколения, контейнеры, поддерживающие параллелизм в JUC, можно охарактеризовать как блокирующие.детализацияПроблема
- Степень детализации блокировки Hashtable и Vector велика (используйте синхронизацию непосредственно в объявлении метода).
- Гранулярность блокировки ConcurrentHashMap, CopyOnWriteArrayList невелика (используйте различные способы обеспечения безопасности потоков, например, мы знаем, что ConcurrentHashMap использует блокировку cas, volatile и другие методы для обеспечения безопасности потоков..)
- Потокобезопасный контейнер под JUC перемещаетсяНе будуВыдает исключение ConcurrentModificationException
Так что в общем будемИспользуйте потокобезопасный контейнер, предоставленный нам в составе пакета JUC., вместо использования потокобезопасного контейнера старого поколения.
Давайте посмотрим, как реализован CopyOnWriteArrayList и почему он используется.обход итераторакогдаНе требует дополнительной блокировки, и не будет вызывать исключение ConcurrentModificationException.
2.1 Принцип реализации CopyOnWriteArrayList
Давайте сначала рассмотрим COW:
Если несколько вызывающих абонентов запрашивают один и тот же ресурс (например, память или хранилище данных на диске) одновременно, они получат его вместе.один и тот же указатель указывает на один и тот же ресурсДо звонящегопопробуй изменитьсодержание ресурса, система будетНастоящая копия выделенной копии(частная копия) вызывающей стороне, в то время как исходный ресурс, видимый другими вызывающими сторонами, остается неизменным.преимуществоэто если звонящийБез модификации ресурса не будет копии(частная копия), поэтому несколько вызывающих абонентов могут только читатьподелиться одним и тем же ресурсом.
Ссылка из Википедии:This.Wikipedia.org/wiki/%E5%AF…
Когда я раньше писал блог, если я хотел посмотреть исходный код, я обычно переводил комментарии к исходному коду и вставлял их в статью с картинками. Эммм, нашел опыт чтения не очень хорошим, так что я здесьПрямая сводка комментариев к исходному кодуСкажите что-то. Кроме того, если вы используете IDEA, вы можете перейти к следующему плагинуTranslation(Бесплатный и простой в использовании).
Обобщите, что описывает исходный комментарий CopyOnWriteArrayList:
- CopyOnWriteArrayList — потокобезопасный контейнер (относительно ArrayList), нижний слойкопировать массивспособ достижения.
- CopyOnWriteArrayList не будет генерировать ConcurrentModificationException при обходе, и нет необходимости добавлять дополнительные блокировки при обходе
- элемент может быть нулевым
2.1.1 Взгляните на базовую структуру CopyOnWriteArrayList
/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
/** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
private transient volatile Object[] array;
/**
* 得到数组
*/
final Object[] getArray() {
return array;
}
/**
* 设置数组
*/
final void setArray(Object[] a) {
array = a;
}
/**
* 初始化CopyOnWriteArrayList相当于初始化数组
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
Это кажется очень простым: нижний слой CopyOnWriteArrayList представляет собой массив, а блокировку выполняет ReentrantLock.
2.1.2 Реализация общих методов
Согласно приведенному выше анализу, мы знаем, что если пройтиVector/SynchronizedList
Его нужно заблокировать вручную.
CopyOnWriteArrayList не нужно отображать блокировку при обходе с помощью итератора, см.add()、clear()、remove()
иget()
Реализация метода может быть немного запутанной.
Сначала мы можем видетьadd()
метод
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;
// 将volatile Object[] array 的指向替换成新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
Через код мы можем узнать, что он заблокирован при добавлении, иСкопируйте новый массив, операция увеличения выполняется для нового массива, и массив указывает на новый массив, и, наконец, разблокировать.
посмотри сноваsize()
метод:
public int size() {
// 直接得到array数组的长度
return getArray().length;
}
посмотри сноваget()
метод:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
Тогда посмотри еще разset()
метод
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 得到原数组的旧值
Object[] elements = getArray();
E oldValue = get(elements, index);
// 判断新值和旧值是否相等
if (oldValue != element) {
// 复制新数组,新值在新数组中完成
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
// 将array引用指向新数组
setArray(newElements);
} else {
// Not quite a no-op; enssures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
заremove()、clear()
иset()和add()
Это похоже, я не буду размещать код здесь.
Суммировать:
- При модификации копируется новый массив, операция модификации завершается в новом массиве, и, наконец, на новый массив указывает переменная массива.
- Запись блокировка, чтение не блокировка
2.1.3 Анализ того, почему вызывающей стороне не нужно явно блокироваться при обходе
У нас есть базовое понимание реализации общего метода, но мы до сих пор не знаем, почему мы можем изменить его, не вызывая исключение при обходе контейнера. Итак, взгляните на его итератор:
// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;
// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组
На данный момент, мы должны быть в состоянии понять это! Когда CopyOnWriteArrayList проходится с помощью итератора, все операцииисходный массив!
2.1.4 Недостатки CopyOnWriteArrayList
Глядя на приведенный выше исходный код реализации, мы, вероятно, сможем проанализировать недостатки CopyOnWriteArrayList.
-
использование памяти: Если CopyOnWriteArrayList часто нужно добавить, удалить или изменить данные в нем, то часто необходимо выполнить
add()、set()、remove()
Если это так, это более интенсивное использование памяти.- потому что мы знаем, что каждый раз
add()、set()、remove()
Эти добавления, удаления и изменения необходимы дляскопировать массивпублично заявить.
- потому что мы знаем, что каждый раз
-
согласованность данных: Контейнер CopyOnWriteМожет быть гарантирована только окончательная согласованность данных, и не может быть гарантирована согласованность данных в реальном времени..
- Из приведенного выше примера также видно, что поток A перебирает данные контейнера CopyOnWriteArrayList. Поток B изменяет данные в части CopyOnWriteArrayList в течение интервала итерации потока A (уже вызванного
setArray()
). Но поток A перебирает исходные данные.
- Из приведенного выше примера также видно, что поток A перебирает данные контейнера CopyOnWriteArrayList. Поток B изменяет данные в части CopyOnWriteArrayList в течение интервала итерации потока A (уже вызванного
2.1.5CopyOnWriteSet
Принцип CopyOnWriteArraySet — CopyOnWriteArrayList.
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
3. Наконец
Чтение этой статьи может потребоватьКонтейнеры Java и многопоточностьИметь некоторые знания. Если вы мало знаете об этих знаниях, вы можете прочитать статьи, которые я написал ранее ~
Если у вас есть лучший способ понять или в статье есть ошибки, пожалуйста, не стесняйтесь оставлять сообщение в области комментариев, чтобы мы могли учиться друг у друга~~~
Использованная литература:
- «Практика параллельного программирования на Java»
- Поговорите о параллелизме — контейнере копирования при записи в Java:если eve.com/Java-copy - о...
- Копировать при записи (COW) в Javanuggets.capable/post/684490…
Дальнейшее чтение:
- Сомневаетесь в методе set класса CopyOnWriteArrayList?если eve.com/copy напишите…
- Why setArray() method call required in CopyOnWriteArrayListstackoverflow.com/questions/2…
Проект с открытым исходным кодом, охватывающий все точки знаний о бэкэнде Java (уже 6 тысяч звезд):GitHub.com/Zhongf UC очень…
если ты хочешьв реальном времениЕсли вы обратите внимание на мои обновленные статьи и галантерейные товары, которыми я делюсь, поищите в WeChat.Java3y.
Содержимое PDF-документоввсе вручную, если вы ничего не понимаете, вы можете напрямуюспросите меня(В официальном аккаунте есть мои контактные данные).