Рубрики
Без рубрики

Заказ потоков на Java

Узнайте о некоторых основных тонкостях заказа Java 8 Stream

Автор оригинала: Sam Millington.

Заказ потоков на Java

1. Обзор

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

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

2. Порядок встречи

Проще говоря, встреча с это порядок, в котором Поток сталкивается с данными .

2.1. Орден коллекций

Коллекционая мы выбираем, как наш источник влияет на порядок встречи поток.

Чтобы проверить это, давайте просто создадим два потока.

Наш первый создан из Список , который имеет внутренний заказ.

Наш второй создан из TreeSet что не так.

Затем мы собираем выход каждого Поток в Массив для сравнения результатов.

@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 не сохранил порядок нашей входной последовательности, поэтому, карабкаясь по порядку встречи Поток .

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

Когда мы повторяем наш тест с помощью параллельных потоков, мы получаем тот же результат:

@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. Удаление заказа

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

Например, давайте объявим 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));

И если мы поток, не вызывая неупорядоченые :

set.stream().parallel().limit(5).toArray();

Потом TreeSet Природный порядок сохраняется:

[-9, -5, -4, -2, 1]

Но, если мы явно удалим заказ:

set.stream().unordered().parallel().limit(5).toArray();

Тогда выход отличается:

[1, 4, 7, 9, 23]

Причина двухкратная: во-первых, поскольку последовательные потоки обработать данные по одному элементу за один раз, неупорядоченые имеет мало влияния на своих собственных. Когда мы позвонили параллельные , тоже, однако, мы повлияли на выход.

3. Промежуточные операции

Мы также можем влияют на заказ потоков через промежуточные операции .

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

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

@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. Операции терминала

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

4.1. ForEach vs ForEachЗаказ

ForEach и ForeachЗаказ может показаться, чтобы обеспечить ту же функциональность, но они имеют одно ключевое различие: ForeachЗаказ гарантии поддержания порядка Поток .

Если мы объявим список:

List list = Arrays.asList("B", "A", "C", "D", "F");

И использовать forEachЗаказ после параллелизации:

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));

Затем выход неупорядоченые :

INFO: C
INFO: F
INFO: B
INFO: D
INFO: A

ForEach регистрирует элементы в порядке, в который они поступают из каждого потока. Вторая Поток с его ForeachЗаказ метод ждет, когда каждый предыдущий поток завершит прежде чем позвонить журнал метод.

4.2. Сбор

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

Например, по своей сути неупорядочено Коллекции такие как TreeSet не будет подчиняться приказу Поток выпуск:

@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()); 
}

При запуске нашего кода, мы видим, что порядок нашей Поток изменения, собирая в набор.

4.3. Определение коллекций

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

Во-первых, мы будем инициализировать наш список, наряду с обычными 2-параметрная версия из 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));
}

Как и ожидалось, наш новый H ashMap не сохранил первоначальный заказ списка входных данных, но давайте изменим его.

С нашей второй Поток , мы будем использовать 4-параметрная версия из toMap метод, чтобы рассказать нашим поставщик поставлять новую 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. Производительность

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

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

Давайте попробуем продемонстрировать это с помощью java Microbenchmark использовать , JMH, для измерения производительности.

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

5.1. Различные

Давайте наймем тест с помощью различные функции как на упорядоченных, так и на неупорядоченых потоках.

@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();
}

Когда мы запускаем, мы видим несоответствие во времени, затя взятом за операцию:

Benchmark                        Mode  Cnt       Score   Error  Units
TestBenchmark.givenOrdered...    avgt    2  222252.283          us/op
TestBenchmark.givenUnordered...  avgt    2   78221.357          us/op

5.2. Фильтр

Далее мы будем использовать параллельную Поток с простым фильтр метод возврата каждые 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();
}

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

Benchmark                        Mode  Cnt       Score   Error  Units
TestBenchmark.givenOrdered...    avgt    2  116333.431          us/op
TestBenchmark.givenUnordered...  avgt    2  111471.676          us/op

6. Заключение

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

Наконец, мы увидели, как контракт на заказ, размещенный на Поток может повлиять на производительность параллельных потоков.

Как всегда, проверить полный набор образцов более на GitHub .