static void statefullParallelLambdaSet() {
        Set<Integer> s = new HashSet<>(
            Arrays.asList(1, 2, 3, 4, 5, 6)
        );

        List<Integer> list = new ArrayList<>();
        int sum = s.parallelStream().mapToInt(e -> {    // pipeline start
            if (list.size() <= 3) {     // list.size() changes while the pipeline operation is executing.
                list.add(e);            // mapToInt's lambda expression depends on this value, so it's stateful.
                return e;
            }
            else return 0;
        }).sum();   // terminal operation
        System.out.println(sum);
    }

В приведенном выше коде говорится, что list.size() изменяется во время работы канала, но я не понимаю.

Поскольку list.add(e) выполняется одновременно в нескольких потоках, потому что он выполняется параллельно, правильно ли предположить, что значение изменяется при каждом его выполнении?

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

Я прав?

1
Powerful_Coder 27 Ноя 2022 в 06:45
Это связано с состоянием гонки, вам нужно понимать параллельные вычисления, мы также должны знать этот материал для рабочих нагрузок HPC, вы никогда не должны полагаться на то, что данные изменяются потоками без использования блокировок baeldung.com/java-concurrent-locks
 – 
Barkermn01
27 Ноя 2022 в 06:53

2 ответа

Таким образом, причина, по которой это происходит, заключается в том, что это называется условиями гонки. ЦП, даже многие многопоточные, запускают больше процессов, чем просто процессы ваших приложений, поэтому он может анализировать и оценивать его, а затем должен спрыгнуть, чтобы сделать что-то для ОС, а затем прийти назад, и другой параллельный процесс для вашего приложения сумел обойти его, потому что ядро/гиперпоток не был украден из его работы.

Вы можете прочитать об условиях гонки в таких книгах, как: link.springer.com/referenceworkentry/10.1007/978-0-387-09766-4_36

Но то, что вы должны сделать, чтобы предотвратить это, - это реализованные блокировки памяти, которую вы изменяете, в Java вы хотите посмотреть на java.util.concurrent.Locks https://www.baeldung.com/java-concurrent-locks

0
Barkermn01 27 Ноя 2022 в 06:58

Ваш код накапливает результат, работая с побочными эффектами, что не рекомендуется документация по Stream API.

В вашем коде вы наткнулись на самый первый пункт из приведенной выше ссылки:

... нет никаких гарантий в отношении:

  • видимость этих побочных эффектов для других потоков;

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

Кроме того, обратите внимание, что операция map() (и все ее разновидности) не предназначена для выполнения побочных эффектов и ее функция соответствует документация должно быть без гражданства:

mapper – немешающая, функция без сохранения состояния, применяемая к каждому элементу.

В этом случае правильным способом включения обработки предыдущих элементов потока будет определение Collector.

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

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

Чтобы реализовать параллельный сборщик, нам нужен потокобезопасный список. Если взглянуть на реализации интерфейса List, вы обнаружите, что JDK предлагает только варианты CopyOnWriteArrayList и устаревшие Verctor.

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

И если бы мы использовали синхронизированный List, это бы ничего не стоило с точки зрения производительности, потому что поток не мог бы работать с этим списком одновременно. Пока один поток добавляет элемент, другие блокируются. Следовательно, блокировка, предложенная в другом ответе, позволит получить только правильный результат, но вы не сможете извлечь выгоду из параллельного выполнения. На самом деле это будет медленнее, чем последовательная обработка данных, потому что блокировка требует затрат.

Что мы можем сделать, так это создать непараллельный Collector на основе простого ArrayList (его все равно можно будет использовать с параллельным потоком, каждый поток будет действовать независимо в отдельном контейнере без блокировки).

Вот как это может выглядеть:

public static Collector<Integer, ?, IntSumContainer> toParallelIntSumContainer(int limit) {
    
    return Collector.of(
        () -> new IntSumContainer(limit),
        IntSumContainer::accept,
        IntSumContainer::merge
    );
}

public class IntSumContainer implements IntConsumer {
    private int sum;
    private List<Integer> list = new ArrayList<>();
    private final int limit;

    public IntSumContainer(int limit) {
        this.limit = limit;
    }

    @Override
    public void accept(int value) {
        if (list.size() < limit) {
            list.add(value);
            sum += value;
        }
    }
    
    public IntSumContainer merge(IntSumContainer other) {
        other.list.stream().limit(limit - list.size()).forEach(this::accept); // there couldn't be issues related to concurrent access in the case, hence performing side-effects via forEach is safe 
        return this;
    }
    
    // getters
}

И вот как будет выглядеть поток:

List<Integer> source = List.of(1, 2, 3, 4, 5, 6);

IntSumContainer result = s.parallelStream()
    .collect(toIntSumContainer(3));

List<Integer> list = result.getList();
int sum = result.getSum();

System.out.println(list);
System.out.println(sum);

Вывод:

[1, 2, 3]
6
0
Alexander Ivanchenko 27 Ноя 2022 в 17:13