Производительность ForkJoinPool Java 8 против 11 - PullRequest
0 голосов
/ 08 января 2019

Рассмотрим следующий фрагмент кода:

package com.sarvagya;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;

public class Streamer {
    private static final int LOOP_COUNT  = 2000;
    public static void main(String[] args){
        try{
            for(int i = 0; i < LOOP_COUNT; ++i){
                poolRunner();
                System.out.println("done loop " + i);
                try{
                    Thread.sleep(50L);
                }
                catch (Exception e){
                    System.out.println(e);
                }
            }
        }
        catch (ExecutionException | InterruptedException e){
            System.out.println(e);
        }

        // Add a delay outside the loop to make sure all daemon threads are cleared before main exits.
        try{
            Thread.sleep(10 * 60 * 1000L);
        }
        catch (Exception e){
            System.out.println(e);
        }
    }

    /**
     * poolRunner method.
     * Assume I don't have any control over this method e.g. done by some library.
     * @throws InterruptedException
     * @throws ExecutionException
     */
    private static void poolRunner() throws InterruptedException, ExecutionException {
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(() ->{
            List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10, 11,12,14,15,16);
            List<Integer> collect = numbers.stream()
                    .parallel()
                    .filter(xx -> xx > 5)
                    .collect(Collectors.toList());
            System.out.println(collect);
        }).get();
    }
}

В приведенном выше коде метод poolRunner создает ForkJoinPool и передает ему некоторые задачи. При использовании Java 8 и сохранении LOOP_COUNT равным 2000, мы могли видеть, что максимальное число созданных потоков было около 3600, как показано ниже Profiling info рис: профилирование

Max Threads in JDK 8 fig: Информация о темах.

Все эти темы снижаются почти до 10 через некоторое время. Тем не менее, в OpenJDK 11 при сохранении того же LOOP_COUNT произойдет следующая ошибка:

[28.822s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
[28.822s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
[28.822s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
Exception in thread "ForkJoinPool-509-worker-5" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
    at java.base/java.lang.Thread.start0(Native Method)
    at java.base/java.lang.Thread.start(Thread.java:803)
    at java.base/java.util.concurrent.ForkJoinPool.createWorker(ForkJoinPool.java:1329)
    at java.base/java.util.concurrent.ForkJoinPool.tryAddWorker(ForkJoinPool.java:1352)
    at java.base/java.util.concurrent.ForkJoinPool.signalWork(ForkJoinPool.java:1476)
    at java.base/java.util.concurrent.ForkJoinPool.deregisterWorker(ForkJoinPool.java:1458)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:187)

Очень скоро достигает максимального предела потока. Сохранение LOOP_COUNT до 500, работает нормально, однако, эти нити очищаются очень очень медленно и достигают плато около 500 нитей. Смотрите изображения ниже:

Thread info in OpenJDK 11 fig: Информация о потоке в OpenJDK 11

Profile Info fig: Профилирование в OpenJDK 11

Потоки были PARKED в JDK 8, но WAIT в JDK 11. Количество потоков демонов также должно быть уменьшено в Java 11, однако это медленно и не работать как положено. Кроме того, предположим, что у меня нет контроля над poolRunner методом. Предположим, этот метод предоставляется какой-то внешней библиотекой.

Это проблема с OpenJDK 11 или я делаю что-то не так в коде. Спасибо.

Ответы [ 2 ]

0 голосов
/ 08 января 2019

Ваш код создает огромное количество ForkJoinPool экземпляров и никогда не вызывает shutdown() ни в одном пуле после его использования. Поскольку в случае Java 8 ничто в спецификации не гарантирует прекращения рабочих потоков, этот код может даже закончиться с 2000 (⟨ число пулов ⟩) раз ⟨ количество ядер ⟩ Темы.

На практике наблюдаемое поведение проистекает из недокументированного времени простоя , равного двум секундам. Обратите внимание, что согласно комментарию, следствием истекшего тайм-аута является попытка сократить количество работников , что отличается от просто прекращения работы. Таким образом, если поток n испытывает таймаут, не все потоки n завершаются, но число потоков уменьшается на один, а остальные потоки могут снова ждать. Кроме того, фраза «начальное значение времени ожидания» уже намекает на это, фактическое время ожидания увеличивается каждый раз, когда это происходит. Таким образом, требуется <em>n</em> * (<em>n</em> + 1) секунд для n незанятого рабочего потока для завершения из-за этого (недокументированного) тайм-аута.

Начиная с Java 9, существует настраиваемый keepAliveTime , который можно указать в новом конструкторе из ForkJoinPool, который также документирует значение по умолчанию:

keepAliveTime
истекшее время с момента последнего использования до завершения потока (а затем позднее, при необходимости, заменено). Для значения по умолчанию используйте 60, TimeUnit.SECONDS.

Эта документация может ввести в заблуждение, полагая, что теперь все рабочие потоки могут завершаться вместе, когда простаивают для keepAliveTime , но на самом деле все еще существует поведение сокращения пула только по одному за раз, хотя сейчас Время не увеличивается. Так что теперь для завершения n простаивающего рабочего потока может потребоваться до 60 * <em>n</em> секунд. Поскольку предыдущее поведение не было определено, это даже не несовместимость.

Следует подчеркнуть, что даже при одном и том же поведении тайм-аута результирующее максимальное количество потоков может измениться, например, когда более новая JVM с лучшей оптимизацией кода сокращает время выполнения реальных операций (без искусственных вставок Thread.sleep(…)) это будет создавать новые потоки быстрее, в то время как завершение все еще связано со временем настенных часов.


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


Вы можете проверить поведение с помощью следующего кода:

int threadNumber = 8;
ForkJoinPool pool = new ForkJoinPool(threadNumber);
// force the creation of all worker threads
pool.invokeAll(Collections.nCopies(threadNumber*2, () -> { Thread.sleep(500); return ""; }));
int oldNum = pool.getPoolSize();
System.out.println(oldNum+" threads; waiting for dying threads");
long t0 = System.nanoTime();
while(oldNum > 0) {
    while(pool.getPoolSize()==oldNum)
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(200));
    long t1 = System.nanoTime();
    oldNum = pool.getPoolSize();
    System.out.println(threadNumber-oldNum+" threads terminated after "
        +TimeUnit.NANOSECONDS.toSeconds(t1 - t0)+"s");
}
Java 8:
8 threads; waiting for dying threads
1 threads terminated after 2s
2 threads terminated after 6s
3 threads terminated after 12s
4 threads terminated after 20s
5 threads terminated after 30s
6 threads terminated after 42s
7 threads terminated after 56s
8 threads terminated after 72s
Java 11:
8 threads; waiting for dying threads
1 threads terminated after 60s
2 threads terminated after 120s
3 threads terminated after 180s
4 threads terminated after 240s
5 threads terminated after 300s
6 threads terminated after 360s
7 threads terminated after 420s

Никогда не завершено, по-видимому, по крайней мере один последний рабочий поток остается живым

0 голосов
/ 08 января 2019

Вы делаете это неправильно.

В приведенном выше коде я создаю ForkJoinPool и отправляю ему несколько задач.

На самом деле вы создаете 2000 ForkJoinPool экземпляров ...

Вместо этого вы должны создать один ForkJoinPool с количеством параллелизма (т. Е. Числом потоков), которое соответствует поставленной задаче.

Создание огромного количества (то есть тысяч) потоков - очень плохая идея. Даже если вы можете сделать это, не вызывая OOME, вы будете использовать много памяти стека и кучи и возлагать большую нагрузку на планировщик и сборщик мусора ... без реальной выгоды.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...