Подтвердить что ты не робот

Почему общая изменчивость плохой?

Я смотрел презентацию на Java, и в какой-то момент лектор сказал:

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

То, что он имел в виду, это следующий фрагмент кода, который он считал "крайне вредной привычкой":

//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(e -> doubleOfEven.add(e));

Затем он продолжил писать код, который должен использоваться, который:

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList());

Я не понимаю, почему первая часть кода - "плохая привычка". Для меня они достигают одной и той же цели.

4b9b3361

Ответ 1

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

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

не-детерминизм = параллельная обработка + изменяемое состояние

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

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

Ответ 2

Дело в том, что лекция слегка неверна в то же время. В примере, который он предоставил, используется forEach, который документируется как:

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

Вы можете использовать:

 numbers.stream()
            .filter(e -> e % 2 == 0)
            .map(e -> e * 2)
            .parallel()
            .forEachOrdered(e -> doubleOfEven.add(e));

И у вас всегда будет тот же гарантированный результат.

С другой стороны, пример, который использует Collectors.toList, лучше, потому что коллекторы уважают encounter order, поэтому он отлично работает.

Интересным моментом является то, что Collectors.toList использует ArrayList под ним, который не является потокобезопасной коллекцией. Он просто использует многие из них (для параллельной обработки) и объединяется в конце.

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

Нам также нужно подумать, что даже использование потоковой безопасности не полностью безопасно для Streams, особенно если вы полагаетесь на side-effects.

 List<Integer> numbers = Arrays.asList(1, 3, 3, 5);
    Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
    List<Integer> collected = numbers.stream()
            .parallel()
            .map(e -> {
                if (seen.add(e)) {
                    return 0;
                } else {
                    return e;
                }
            })
            .collect(Collectors.toList());

    System.out.println(collected);

collected в этой точке может быть [0,3,0,0] OR [0,0,3,0] или что-то еще.

Ответ 3

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

Первый поток создает doubleOfEven. Второй поток создает doubleOfEven, экземпляр, созданный первым потоком, будет собираться мусором. Затем оба потока добавят двойные числа всех четных чисел в doubleOfEvent, поэтому он будет содержать 0, 0, 4, 4, 8, 8, 12, 12,... вместо 0, 4, 8, 12... ( В действительности эти потоки не будут полностью синхронизированы, поэтому все, что может пойти не так, пойдет не так).

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