Порядок потоков в Java
1. обзор
В этом руководстве мы рассмотрим, какdifferent uses of the Java Stream API affect the order in which a stream generates, processes, and collects data.
Мы также посмотрим, какordering influences performance.
2. Порядок встречи
Проще говоря,encounter order - это the order in which a Stream encounters data.
2.1. Порядок встреч источниковCollection
Collection, который мы выбираем в качестве источника, влияет на порядок встречиStream.
Чтобы проверить это, давайте просто создадим два потока.
Наш первый создается изList, который имеет внутренний порядок.
Наш второй создан изTreeSet, которого нет.
Затем мы собираем выходные данные каждогоStream вArray, чтобы сравнить результаты.
@Test
public void givenTwoCollections_whenStreamedSequentially_thenCheckOutputDifferent() {
List list = Arrays.asList("B", "A", "C", "D", "F");
Set set = new TreeSet<>(list);
Object[] listOutput = list.stream().toArray();
Object[] setOutput = set.stream().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
Как мы можем видеть из нашего примера,TreeSet не соблюдает порядок нашей входной последовательности, следовательно, скремблирует порядок встречиStream.
Если нашStream упорядочен, тоdoesn’t matter whether our data is being processed sequentially or in parallel; реализация будет поддерживать порядок встречиStream.
Когда мы повторяем наш тест, используя параллельные потоки, мы получаем тот же результат:
@Test
public void givenTwoCollections_whenStreamedInParallel_thenCheckOutputDifferent() {
List list = Arrays.asList("B", "A", "C", "D", "F");
Set set = new TreeSet<>(list);
Object[] listOutput = list.stream().parallel().toArray();
Object[] setOutput = set.stream().parallel().toArray();
assertEquals("[B, A, C, D, F]", Arrays.toString(listOutput));
assertEquals("[A, B, C, D, F]", Arrays.toString(setOutput));
}
2.2. Удаление заказа
В любой момент мы можемexplicitly remove the order constraint with the unordered method.
Например, давайте объявимTreeSet:
Set set = new TreeSet<>(
Arrays.asList(-9, -5, -4, -2, 1, 2, 4, 5, 7, 9, 12, 13, 16, 29, 23, 34, 57, 102, 230));
И если мы будем транслировать без вызоваunordered:
set.stream().parallel().limit(5).toArray();
Тогда естественный порядокTreeSet сохраняется:
[-9, -5, -4, -2, 1]
Но, если мы явно удалим порядок:
set.stream().unordered().parallel().limit(5).toArray();
Тогда результат будет другим:
[1, 4, 7, 9, 23]
Причина двоякая: во-первых, поскольку последовательные потоки обрабатывают данные по одному элементу за раз,unordered сам по себе мало влияет. Однако когда мы вызывалиparallel, это также повлияло на вывод.
3. Промежуточные операции
Мы также можемaffect stream ordering through intermediate operations.
Хотя большинство промежуточных операций будут поддерживать порядокStream,, некоторые по своей природе изменят его.
Например, мы можем повлиять на порядок потоков, отсортировав:
@Test
public void givenUnsortedStreamInput_whenStreamSorted_thenCheckOrderChanged() {
List list = Arrays.asList(-3, 10, -4, 1, 3);
Object[] listOutput = list.stream().toArray();
Object[] listOutputSorted = list.stream().sorted().toArray();
assertEquals("[-3, 10, -4, 1, 3]", Arrays.toString(listOutput));
assertEquals("[-4, -3, 1, 3, 10]", Arrays.toString(listOutputSorted));
}
4. Терминальные операции
Наконец, мы можем повлиять на порядокdepending on the terminal operation that we use.
4.1. ForEach vsForEachOrdered
ForEach andForEachOrdered может показаться с той же функциональностью, но у них есть одно ключевое отличие:ForEachOrdered guarantees to maintain the order of the Stream.
Если мы объявим список:
List list = Arrays.asList("B", "A", "C", "D", "F");
И используйтеforEachOrdered после распараллеливания:
list.stream().parallel().forEachOrdered(e -> logger.log(Level.INFO, e));
Тогда вывод упорядочен:
INFO: B
INFO: A
INFO: C
INFO: D
INFO: F
Однако, если использоватьforEach:
list.stream().parallel().forEach(e -> logger.log(Level.INFO, e));
Тогда результат будетunordered:
INFO: C
INFO: F
INFO: B
INFO: D
INFO: A
ForEach регистрирует элементы в порядке их поступления из каждого потока. ВторойStream переключаетForEachOrdered method waits for each previous thread to complete перед вызовом методаlog .
4.2. Collectс
Когда мы используем методcollect для агрегирования выходных данныхStream , важно отметить, что выбранный намиCollection повлияет на порядок.
Например, выводinherently unordered Collections such as TreeSet won’t obey the order of the Stream:
@Test
public void givenSameCollection_whenStreamCollected_checkOutput() {
List list = Arrays.asList("B", "A", "C", "D", "F");
List collectionList = list.stream().parallel().collect(Collectors.toList());
Set collectionSet = list.stream().parallel()
.collect(Collectors.toCollection(TreeSet::new));
assertEquals("[B, A, C, D, F]", collectionList.toString());
assertEquals("[A, B, C, D, F]", collectionSet.toString());
}
При запуске нашего кода мы видим, что порядок нашихStream изменяется, собираясь вSet.
4.3. УказаниеCollections
В случае, если мы собираем в неупорядоченную коллекцию, используя, скажем,Collectors.toMap, мы все равно можем принудительно упорядочить поchanging the implementation of our Collectors methods to use the Linked implementation.
Сначала мы инициализируем наш список вместе с обычным2-parameter version методаtoMap:
@Test
public void givenList_whenStreamCollectedToHashMap_thenCheckOrderChanged() {
List list = Arrays.asList("A", "BB", "CCC");
Map hashMap = list.stream().collect(Collectors
.toMap(Function.identity(), String::length));
Object[] keySet = hashMap.keySet().toArray();
assertEquals("[BB, A, CCC]", Arrays.toString(keySet));
}
Как и ожидалось, наш новыйHashMap не сохранил исходный порядок списка ввода, но давайте это изменим.
С нашим вторымStream мы будем использовать4-parameter version из методаtoMap , чтобы сообщить нашемуsupplier to новыйLinkedHashMap:
@Test
public void givenList_whenCollectedtoLinkedHashMap_thenCheckOrderMaintained(){
List list = Arrays.asList("A", "BB", "CCC");
Map linkedHashMap = list.stream().collect(Collectors.toMap(
Function.identity(),
String::length,
(u, v) -> u,
LinkedHashMap::new
));
Object[] keySet = linkedHashMap.keySet().toArray();
assertEquals("[A, BB, CCC]", Arrays.toString(keySet));
}
Эй, это намного лучше!
Нам удалось сохранить исходный порядок списка, собрав данные доLinkedHashMap.
5. Спектакль
Если мы используем последовательные потоки, наличие или отсутствие порядка мало влияет на производительность нашей программы. Parallel streams, however, can be heavily affected by the presence of an ordered Stream.с
Причина в том, что каждый поток должен ждать вычисления предыдущего элементаStream.
Давайте попробуем продемонстрировать это с помощьюJava Microbenchmark harness, JMH, для измерения производительности.
В следующих примерах мы измерим затраты на производительность обработки упорядоченных и неупорядоченных параллельных потоков с помощью некоторых общих промежуточных операций.
5.1. Distinctс
Давайте настроим тест с использованием функцииdistinct как для упорядоченных, так и для неупорядоченных потоков.
@Benchmark
public void givenOrderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).parallel().distinct().toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamDistinct_thenShowOpsPerMS() {
IntStream.range(1, 1_000_000).unordered().parallel().distinct().toArray();
}
Когда мы нажимаем run, мы можем видеть несоответствие во времени, затрачиваемом на операцию:
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 222252.283 us/op
TestBenchmark.givenUnordered... avgt 2 78221.357 us/op
5.2. Filter с
Затем мы будем использовать параллельныйStream с простым методомfilter , чтобы возвращать каждое 10-е целое число:.
@Benchmark
public void givenOrderedStreamInput_whenStreamFiltered_thenShowOpsPerMS() {
IntStream.range(1, 100_000_000).parallel().filter(i -> i % 10 == 0).toArray();
}
@Benchmark
public void givenUnorderedStreamInput_whenStreamFiltered_thenShowOpsPerMS(){
IntStream.range(1,100_000_000).unordered().parallel().filter(i -> i % 10 == 0).toArray();
}
Интересно, что разница между нашими двумя потоками намного меньше, чем при использовании методаdistinct .
Benchmark Mode Cnt Score Error Units
TestBenchmark.givenOrdered... avgt 2 116333.431 us/op
TestBenchmark.givenUnordered... avgt 2 111471.676 us/op
6. Заключение
В этой статье мы рассмотрели сортировку потоков , сосредоточив внимание наthe different stages of the Stream process and how each one has its own effect.
Наконец, мы увидели, какorder contract placed on a Stream can affect the performance of parallel streams.
Как всегда, проверьте полный набор образцовover on GitHub.