Согласованность памяти после выхода из рабочих потоков параллельного потока Java

Учитывая следующий код:

final int n = 50;
final int[] addOne = new int[n];
IntStream.range(0, n)
        .parallel()
        .forEach(i -> addOne[i] = i + 1);
// (*) Are the addOne[i] values all visible here?
for (int value : addOne) {
    System.out.println(value);
}

Вопрос: можно ли гарантировать, что основной поток увидит все содержимое массива, записанное рабочими потоками, после выхода из рабочих потоков (т. е. в точке (*))?

Мне интересно понять, что модель памяти Java говорит о вышеупомянутом вопросе. Это не имеет ничего общего с проблемами параллелизма как таковыми (т.е. с тем фактом, что параллельные потоки в Java могут обрабатывать свои элементы в любом порядке). Чтобы предупредить некоторые ответы, я знаю, что невозможно гарантировать семантику упорядочения памяти между двумя разными потоками с доступом к одному и тому же элементу массива в Java без использования чего-то вроде AtomicReferenceArray<E>. В целях ответа на этот вопрос предположим, что Atomic* классов не будут использоваться параллельными рабочими процессами. Что еще более важно, обратите внимание, что никакие два рабочих потока никогда не пытаются писать в один и тот же элемент массива, поскольку все значения i уникальны. Поэтому семантика упорядочения памяти между потоками здесь не важна, а важна только то, будет ли какое-либо значение, записанное в элемент массива рабочим потоком, всегда быть видимым для основного потока после завершения параллельного потока.

Существует вычислительный «барьер» между инициализацией элементов массива в основном потоке и запуском параллельных рабочих потоков (рабочие всегда сначала будут видеть элементы с нулевым значением инициализатора). И есть барьер завершения, который ожидает завершения всех рабочих процессов в конце потока, прежде чем передать управление обратно основному потоку. Таким образом, на самом деле вопрос сводится к можно ли предполагать полное упорядочение или неявный «барьер сброса памяти», когда вычислительный барьер накладывается в конце параллельного потока.

На другой вопрос: есть ли вообще шанс, что основной поток может прочитать значение инициализации по умолчанию 0 для некоторого элемента после точки (*)? Или иерархия кеш-памяти ЦП всегда будет гарантировать, что основной поток увидит самое последнее значение, записанное в массив рабочим потоком, даже если это значение еще не было сброшено из кеш-памяти ЦП обратно в ОЗУ?

Для целей этого вопроса я предполагаю, что для возврата управления в основной поток после завершения параллельного потока требуется нулевое время, поэтому нет состояния гонки, которое могло бы привести к тому, что значения массива будут сброшены в ОЗУ из-за времени это требуется для выключения параллельного потока, или из-за количества вытеснения кеша, которое должно произойти, чтобы выключить параллельный поток.


person Luke Hutchison    schedule 21.02.2020    source источник
comment
Отвечает ли это на ваш вопрос? Почему существует Collection.parallelStream (), когда. stream (). parallel () делает то же самое?   -  person vicpermir    schedule 21.02.2020
comment
@vicpermir не совсем, кажется, это не связано.   -  person Luke Hutchison    schedule 22.02.2020
comment
Кстати, если N не является константой, его нужно писать в нижнем регистре ...   -  person dan1st    schedule 23.02.2020
comment
Поскольку порядок встречи не играет в этом роль, вопрос сводится к тому, был ли потребитель, указанный в вызове forEach, выполнен для всех элементов Stream. Я уверен, что это так (хотя я не буду искать на это авторитетного ответа).   -  person daniu    schedule 24.02.2020
comment
Я не могу закрыть это как дубликат, но вот и все   -  person Eugene    schedule 13.04.2020


Ответы (2)


JMM говорит :

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

Это означает, что вам нужно убедиться, что между записью и чтением элементов массива существует связь «произошло раньше».

Javadoc метода java.util.stream.IntStream#forEach говорит:

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

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

PS: Streams - это сложная структура, и, на самом деле, я не уверен, действительно ли это небезопасно в вашей конкретной ситуации, но в контракте говорится, что нет гарантии, если вы получаете доступ к общему состоянию (ваш массив используется совместно между потоком вызывающего и рабочими), и лучше следовать контракту.

person Aleksandr Semyannikov    schedule 21.02.2020
comment
Во втором разделе, который вы процитировали, говорится, что элементы могут обрабатываться в любом порядке, который не имеет ничего общего с моделью памяти Java, только то, что реализация параллельных потоков оставляет за собой право работать с элементами в любом порядке. Когда поток завершится, все элементы будут обработаны, но при этом остается нерешенным вопрос о согласованности памяти. Вы сказали: вы должны обеспечить связь «происходит раньше» между записью и чтением элементов массива. В конце потока уже есть барьер завершения. Разве это не приводит к отношениям «случилось раньше»? - person Luke Hutchison; 22.02.2020
comment
@LukeHutchison, вы пропустили часть. Если действие обращается к общему состоянию, оно отвечает за обеспечение необходимой синхронизации, элементы вашего массива являются разделяемым состоянием. Эта часть о порядке обработки не важна для вашего вопроса. - person Aleksandr Semyannikov; 22.02.2020
comment
Я понимаю, что вы говорите, но общее состояние влияет только на ситуации, когда у вас есть что-то, кроме одного писателя или любого количества читателей в данный момент. Как только вы смешиваете читателей и писателей и / или имеете несколько писателей для одного фрагмента памяти, вы должны обеспечить синхронизацию для общего состояния. Это стандартный и общий принцип параллелизма, который не имеет ничего общего с моделью памяти Java как таковой. Я хочу знать, можно ли считать, что кеши ЦП согласованы в конце потока, чтобы глобальный поток видел последние значения кеша. - person Luke Hutchison; 23.02.2020
comment
@LukeHutchison, есть два автора каждого элемента, сначала каждый элемент устанавливается в 0 в основном потоке во время инициализации массива, затем он заменяется одним из рабочих потоков. - person Aleksandr Semyannikov; 23.02.2020
comment
У вас может быть любое количество писателей. Но у вас не может быть двух или более одновременных писателей. Существует строгий общий порядок (происходит после) между всеми происходящими инициализационными операциями записи и запуском всех рабочих потоков. Также существует строгий общий порядок между записью рабочих потоков и чтением после завершения потока. Между разными рабочими записями нет никакого упорядочивания. Но для любого конкретного элемента массива существует полный порядок между инициализацией значения, перезаписью и последующим чтением. Нет никакой путаницы в отношении общего порядка. - person Luke Hutchison; 23.02.2020

Проверенный ответ:

Пул Fork-Join, в котором выполнение конвейера после того, как parallel() происходит, имеет шаги fork() invoke() и join(), а последний шаг в этой последовательности join() семантически эквивалентен Thread.join(), что означает, что есть произошло-раньше семантика между параллельной задачей, выполняемой пулом Fork-Join, и оператором после нее.

person diginoise    schedule 24.02.2020
comment
Нет необходимости в настраиваемом пуле потоков. Параллельный поток никогда не возвращает управление вызывающему потоку до тех пор, пока все рабочие потоки не станут неподвижными после завершения обработки всех элементов потока. Да, Future<T> можно использовать для создания абсолютного упорядочения между писателем и читателями, но это не тот вопрос, который я здесь задаю. - person Luke Hutchison; 25.02.2020
comment
@LukeHutchison Теперь я понял, что вы имеете в виду - напишите видимость при возможном изменении порядка операций и что это гарантирует. - person diginoise; 25.02.2020