Почему потоковые решения на основе AtomicInteger не рекомендуются? - PullRequest
0 голосов
/ 16 ноября 2018

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

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 законнымway?
Java 8: Предпочтительный способ подсчета итераций лямбды?

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

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

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

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

Ответы [ 3 ]

0 голосов
/ 16 ноября 2018

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

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

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

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

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

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

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

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

0 голосов
/ 16 ноября 2018

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

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

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


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

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

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

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

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

Это не тот случай. Вы, вероятно, не столкнетесь с проблемами.

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

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


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

0 голосов
/ 16 ноября 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

...