Почему решения Stream на основе AtomicInteger не рекомендуются?

Скажем, у меня есть этот список фруктов: -

List<String> f = Arrays.asList("Banana", "Apple", "Grape", "Orange", "Kiwi");

Мне нужно добавить серийный номер к каждому фрукту и распечатать его. Порядок фруктов или порядковый номер не имеет значения. Итак, это правильный вывод: -

4. Kiwi
3. Orange
1. Grape
2. Apple
5. Banana

Решение №1

AtomicInteger number = new AtomicInteger(0);

String result = f.parallelStream()
        .map(i -> String.format("%d. %s", number.incrementAndGet(), i))
        .collect(Collectors.joining("\n"));

Решение №2

String result = IntStream.rangeClosed(1, f.size())
        .parallel()
        .mapToObj(i -> String.format("%d. %s", i, f.get(i - 1)))
        .collect(Collectors.joining("\n"));

Вопрос

Почему решение № 1 является плохой практикой? Во многих местах я видел, что решения на основе AtomicInteger плохи (например, в этом ответе), особенно при параллельной потоковой обработке (по этой причине я использовал параллельные потоки выше, чтобы попытаться столкнуться с проблемами).

Я просмотрел эти вопросы/ответы: -
В каких случаях Stream операции должны иметь состояние?
Является ли использование AtomicInteger для индексации в Stream законным способом?
Java 8: предпочтительный способ подсчета итераций лямбды?

Они просто упоминают (если я что-то не пропустил) «могут произойти неожиданные результаты». Как, например? Может ли это произойти в этом примере? Если нет, можете ли вы привести пример, где это может произойти?

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

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

Может ли кто-нибудь привести примеры, в каких случаях возникнут проблемы при использовании такого решения на основе состояния?


person Kartik    schedule 16.11.2018    source источник
comment
tldr: Побочные эффекты неприятны, даже если потокобезопасны. Аргумент порядка очень актуален в более общих случаях: например. вместо добавления целых чисел (x + y == y + x), что, если бы он объединял строки (concat(x,y) != concat(y,x))? Шансов случайно ввести такое гораздо меньше, если избежать побочных эффектов.   -  person user2864740    schedule 16.11.2018
comment
Из документов неясно, требуется ли безгражданство или только рекомендуется. Я лично не вижу проблем с № 1, хотя могу представить, что № 2 работает лучше.   -  person shmosel    schedule 16.11.2018
comment
Потоки исходят из функционального программирования, где в идеале у вас не должно быть побочных эффектов. Иногда это невозможно, но если есть простой способ добиться того же самого без побочных эффектов, вы должны использовать его.   -  person Peter Lawrey    schedule 16.11.2018
comment
Что ж, когда вас устраивает результат, в котором числа расположены не по порядку и не отражают порядок исходных элементов, единственная оставшаяся проблема заключается в том, что это неэффективно по сравнению с рекомендуемым подходом. Но большинство других вопросов и ответов посвящены задачам, которые не подходят для такого неправильного порядка, и что ж, когда вы используете этот шаблон в тех редких случаях, когда вас устраивает бессмысленное число, это может вскоре стать привычкой…   -  person Holger    schedule 16.11.2018


Ответы (3)


Также обратите внимание, что попытка получить доступ к изменяемому состоянию из поведенческих параметров представляет собой плохой выбор с точки зрения безопасности и производительности; если вы не синхронизируете доступ к этому состоянию, у вас возникает гонка данных и, следовательно, ваш код неисправен, но если вы синхронизируете доступ к этому состоянию, вы рискуете, что конфликты подорвут параллелизм, от которого вы стремитесь извлечь выгоду.< /strong> Наилучший подход — полностью отказаться от поведенческих параметров с отслеживанием состояния для потоковой передачи операций; обычно есть способ реструктурировать потоковый конвейер, чтобы избежать сохранения состояния.

Пакет java.util.stream, поведение без сохранения состояния

С точки зрения потокобезопасности и корректности в решении 1 нет ничего плохого. Однако производительность (как преимущество параллельной обработки) может пострадать.


Почему решение № 1 является плохой практикой?

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

Они просто упоминают (если я что-то не пропустил) «могут произойти неожиданные результаты». Как, например?

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

Может ли это произойти в этом примере?

Это не так. Скорее всего, вы не столкнетесь с проблемами.

Если нет, можете ли вы привести пример, где это может произойти?

Измените AtomicInteger на int*, замените number.incrementAndGet() на ++number, и он у вас будет.


*упакованный int (например, на основе оболочки, на основе массива), чтобы вы могли работать с ним в лямбда-выражении

person Andrew Tobilko    schedule 16.11.2018

Посмотрите, что ответил Стюарт Маркс здесь - он использует предикат с отслеживанием состояния.

Это пара потенциальных проблем, но если вы не заботитесь о них или действительно понимаете их, все должно быть в порядке.

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

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

Третий более тонкий. Иногда нет гарантии, что map вообще будет выполнено, например, начиная с java-9:

 someStream.map(i -> /* do something with i and numbers */)
           .count();

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

В вашем примере, ИМО, вы можете более чем безопасно делать то, что делаете; но если вы немного измените свой код, это потребует дополнительных рассуждений, чтобы доказать его правильность. Я бы выбрал решение 2 просто потому, что мне его намного легче понять, и оно не имеет потенциальных проблем, перечисленных выше.

person Eugene    schedule 16.11.2018

Случай 2. В примечаниях к API класса IntStream возвращается последовательный упорядоченный IntStream от startInclusive (включительно) до endInclusive (включительно) с шагом 1 вида цикла for, поэтому параллельный поток обрабатывает его один за другим и обеспечивает правильный порядок.

 * @param startInclusive the (inclusive) initial value
 * @param endInclusive the inclusive upper bound
 * @return a sequential {@code IntStream} for the range of {@code int}
 *         elements
 */
public static IntStream rangeClosed(int startInclusive, int endInclusive) {

Случай 1. Очевидно, что список будет обрабатываться параллельно, поэтому порядок будет неправильным. Поскольку операция сопоставления выполняется параллельно, результаты для одного и того же ввода могут варьироваться от запуска к запуску из-за различий в планировании потоков, поэтому нет гарантий, что разные операции над «одним и тем же» элементом в одном и том же потоковом конвейере также выполняются в одном и том же потоке. нет никакой гарантии, что функция сопоставления также применяется к конкретным элементам в потоке.

Исходный документ Java

person zack    schedule 16.11.2018
comment
@Kartik: нет никаких гарантий, что разные операции с одним и тем же элементом в одном конвейере потока выполняются в одном потоке. Таким образом, нет никакой гарантии, как функция сопоставления также применяется к конкретным элементам в потоке или в каком потоке выполняется какой-либо поведенческий параметр. Таким образом, результат также может быть неожиданным. - person zack; 16.11.2018