2015-02-25 2 views
48

Запуск следующего потока пример Java8:Java8 потоки последовательного и параллельного исполнения дают разные результаты?

System.out.println(Stream 
     .of("a", "b", "c", "d", "e", "f") 
     .reduce("", (s1, s2) -> s1 + "/" + s2) 
    ); 

выходы:

/a/b/c/d/e/f 

Который есть - конечно - не удивительно. Благодаря http://docs.oracle.com/javase/8/docs/api/index.html?overview-summary.html не должно иметь значения поток выполняется ли последовательно или параллельно:

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

AFAIK reduce() детерминирована и (s1, s2) -> s1 + "/" + s2 ассоциативно, так что добавление parallel() должно дать тот же результат:

System.out.println(Stream 
      .of("a", "b", "c", "d", "e", "f") 
      .parallel() 
      .reduce("", (s1, s2) -> s1 + "/" + s2) 
    ); 

Однако результат на моей машине:

/a//b//c//d//e//f 

Что здесь не так ?

BTW: использование (предпочтительный) .collect(Collectors.joining("/")) вместо дает тот же результат a/b/c/d/e/f для последовательного и параллельного выполнения.

детали JVM:

java.specification.version: 1.8 
java.version: 1.8.0_31 
java.vm.version: 25.31-b07 
java.runtime.version: 1.8.0_31-b13 
+0

На самом деле это [специально указано] (http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#reduce-java.util.function.BinaryOperator -), что если ваш 'BinaryOperator' является [Ассоциативным] (http://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Associativity), то это должно работать с параллельные потоки. – OldCurmudgeon

+4

В дополнение к тому, чтобы не следовать правилам сокращения, как указывали другие ответчики, есть более простой способ сделать то, что вы делаете: 'stream.collect (joining ("/"))' –

ответ

61

Из документации сократить годы:

значение Идентичность должно быть тождеством для функции аккумулятора. Это означает, что для всех t, accumulator.apply (identity, t) равен t.

Что не так в вашем случае - "" и "a" создает "/ a".

Я извлек агрегатную функцию и добавил распечатку, чтобы показать, что происходит:

BinaryOperator<String> accumulator = (s1, s2) -> { 
    System.out.println("joining \"" + s1 + "\" and \"" + s2 + "\""); 
    return s1 + "/" + s2; 
}; 
System.out.println(Stream 
       .of("a", "b", "c", "d", "e", "f") 
       .parallel() 
       .reduce("", accumulator) 
); 

Это пример вывода (она отличается между запусками):

joining "" and "d" 
joining "" and "f" 
joining "" and "b" 
joining "" and "a" 
joining "" and "c" 
joining "" and "e" 
joining "/b" and "/c" 
joining "/e" and "/f" 
joining "/a" and "/b//c" 
joining "/d" and "/e//f" 
joining "https://stackoverflow.com/a//b//c" and "/d//e//f" 
/a//b//c//d//e//f 

Вы можете добавить, если заявление к вашей функции для обработки пустой строки отдельно:

System.out.println(Stream 
     .of("a", "b", "c", "d", "e", "f") 
     .parallel() 
     .reduce((s1, s2) -> s1.isEmpty()? s2 : s1 + "/" + s2) 
); 

As Marko Topolnik noti ced, проверка s2 не требуется, так как аккумулятор не должен быть коммутативной функцией.

+0

Nice! Таким образом, использование 'reduce', которое не использует идентификатор, также устраняет проблему' .reduce ((s1, s2) -> s1 + "/" + s2) '. – OldCurmudgeon

+2

Предпочиталось условное выражение, IMHO: '(s1, s2) -> s1.isEmpty()? s2: s2.isEmpty()? s1: s1 + "/" + s2' –

+3

Кроме того, поскольку функция аккумулятора не должна быть коммутативной, вам не нужно проверять 's2' на пустое. Итак, '(s1, s2) -> s1.isEmpty()? s2: s1 + "/" + s2'. –

6

Чтобы добавить другой ответ,

Вы можете использовать Мутабельное сокращение, док указать, что делать что-то вроде

String concatenated = strings.reduce("", String::concat) 

Даст плохой результат производительности.

Мы получили желаемый результат, и он будет работать параллельно. Тем не менее, мы можем быть не довольны производительностью! Такая реализация будет выполнять большое количество строковых копий, а время выполнения будет равно O (n^2) в количестве символов. Более эффективным подходом было бы накопление результатов в StringBuilder, , который является изменяемым контейнером для накопления строк. Мы можем использовать метод для параллелизации сменного редукции, как это делается с обычным уменьшением .

Таким образом, вместо этого вы должны использовать StringBuilder.

2

Для тех, кто только начинал с лямбда и ручейков, потребовалось довольно много времени, чтобы добраться до момента «AHA», пока я не понял, что здесь происходит. Я немного перефразирую это, чтобы немного облегчить (по крайней мере, как мне хотелось бы, чтобы это было действительно ответили) для новичков потока, подобных мне.

Это все по документации сократить, где говорится:

значение Идентификатор должен быть тождеством для функции аккумулятора. Это означает, что для всех t, accumulator.apply (identity, t) равен t.

Мы можем легко доказать, что путь код, ассоциативность сломана:

static private void isAssociative() { 
    BinaryOperator<String> operator = (s1, s2) -> s1 + "/" + s2; 
    String result = operator.apply("", "a"); 
    System.out.println(result); 
    System.out.println(result.equals("a")); 
} 

пустая строка сцепленная с другой строкой, действительно должен произвести вторую строку; что не происходит, поэтому накопитель (BinaryOperator) НЕ ассоциативен, и поэтому метод уменьшения не может гарантировать тот же результат в случае параллельного вызова.

+2

Ассоциативность оператора не нарушена, единственная проблема связана с идентификационным значением. См. Https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Associativity – lbalazscs

Смежные вопросы