Кэш память потоков и оперативная память
Есть следующий код, поток просто добавляет числа по возрастанию в список:
List<Integer> integers = new ArrayList<>();
ListThread listThread1 = new ListThread(integers);
ListThread listThread2 = new ListThread(integers);
ListThread listThread3 = new ListThread(integers);
ListThread listThread4 = new ListThread(integers);
ListThread listThread5 = new ListThread(integers);
ListThread listThread6 = new ListThread(integers);
ListThread listThread7 = new ListThread(integers);
listThread1.start();
listThread2.start();
listThread3.start();
listThread4.start();
listThread5.start();
listThread6.start();
listThread7.start();
listThread1.join();
listThread2.join();
listThread3.join();
listThread4.join();
listThread5.join();
listThread6.join();
listThread7.join();
System.out.println(integers);
Выводится следующая ошибка:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: Index 171 out of bounds for length 163
at java.base/java.util.ArrayList.add(ArrayList.java:455)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
Вопрос:
как я понял, у каждого потока свой кэш и у каждого в кэше хранится "своя версия" полей integers, а сам список integers хранится в оперативной памяти. Как тогда происходит это взаимодействие информации из кэша и информации из оперативной памяти, ведь если поток хочет положить какой-то элемент за пределы массива, то значит в кэше этого потока это массив уже увеличен. И как формируется список в нашей оперативной памяти, если в кэшах есть множество его вариантов, какой вариант попадает в оперативную память?
public class ListThread extends Thread {
private final List<Integer> list;
public ListThread(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
for (int i = 0; i < 400; i++) {
list.add(i);
}
}
}
Ответы (2 шт):
ArrayList просто не потокобезопасный объект. Используйте потокобезопасные коллекции для работы со списком из нескольких потоков, варианты есть например на английском СО.
Например, можно использовать Collections.synchronizedList.
Тут дело в том, что потоки при выполнении своей работы конкурируют между собой, а коллекция ArrayList, как правильно заметили в другом ответе, не является потокобезопасной. Так выглядит метод add() класса ArrayList, который вы используете для добавления элементов:
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
а так выглядит приватный метод add(), на который он опирается:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
Потоки конкуррентно выполняют вызов публичного метода add(), и при такой их работе могут быть разные "неожиданные" эффекты. Может быть брошено исключение, а может быть и не брошено, зато в окончательном списке окажутся далеко не все элементы.
Разберём случай с исключением. Все потоки одновременно вызывают add(), который в свою очередь вызывает приватный add(), в котором блок if не выполняется (поначалу size меньше размера массива), элемент добавляется и переменная size наращивается. Может произойти так, что все потоки одновременно нарастят size таким образом, что она перескочит через elementData.length, превысив фактический размер массива. И при следующем вызове приватного метода add() блок if всё так же не выполнится, соответственно, массив не расширится, соответственно, будем пытаться добавить элемент по индексу, превышающему размер массива, отсюда и исключение.
Но есть и другая упомянутая выше ситуация. Потоки, вызывая add() одновременно и не ожидая друг друга, зачастую будут вызывать его с одинаковым size, так как за счёт того, что не все add()-ы отработали, она не нарастится нужное количество раз. Отсюда получаем добавление на одинаковые позиции и "неполноценный" окончательный список в том случае, если программа отработает и исключение не будет брошено.