Спеціальний пул потоків у паралельному потоці Java 8


398

Чи можна вказати спеціальний пул потоків для паралельного потоку Java 8 ? Я не можу його знайти ніде.

Уявіть, що у мене є серверна програма, і я хотів би використовувати паралельні потоки. Але додаток великий і багатопотоковий, тому я хочу його розділити. Я не хочу повільно виконуваного завдання в одному модулі завдань блокування додатків з іншого модуля.

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

Спробуйте наступний приклад. Існують деякі інтенсивні завдання процесора, виконані в окремих потоках. Завдання використовують паралельні потоки. Перше завдання порушено, тому кожен крок займає 1 секунду (імітується режимом сну). Проблема полягає в тому, що інші нитки застрягають і чекають, коли зламане завдання закінчиться. Це надуманий приклад, але уявіть додаток сервлетів і хтось, хто подає тривале завдання в пул приєднання до спільної вилки.

public class ParallelTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> runTask(1000)); //incorrect task
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));


        es.shutdown();
        es.awaitTermination(60, TimeUnit.SECONDS);
    }

    private static void runTask(int delay) {
        range(1, 1_000_000).parallel().filter(ParallelTest::isPrime).peek(i -> Utils.sleep(delay)).max()
                .ifPresent(max -> System.out.println(Thread.currentThread() + " " + max));
    }

    public static boolean isPrime(long n) {
        return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
    }
}

3
Що ви маєте на увазі під власним пулом потоків? Існує один загальний ForkJoinPool, але ви завжди можете створити свій власний ForkJoinPool і подати до нього запити.
оприлюднений

7
Підказка: Чемпіон Java Хайнц Кабуц вивчає ту саму проблему, але з ще гіршим впливом. Дивіться javaspecialists.eu/archive/Issue223.html
Peti

Відповіді:


395

Насправді є хитрість, як виконати паралельну операцію в конкретному пулі fork-join. Якщо ви виконаєте його як завдання в пулі fork-join, він залишається там і не використовує загальний.

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

Трюк заснований на ForkJoinTask.fork, який вказує: "Впорядковує асинхронно виконувати це завдання в пулі, в якому виконується поточне завдання, якщо це можливо, або використовуючи ForkJoinPool.commonPool (), якщо не inForkJoinPool ()"


20
Деталі щодо рішення описані тут blog.krecan.net/2014/03/18/…
Лукас

3
Але чи вказано також, що потоки використовують ForkJoinPoolабо це деталі реалізації? Посилання на документацію було б непогано.
Миколай

6
@Lukas Дякуємо за фрагмент Додам, що ForkJoinPoolекземпляр повинен бути, shutdown()коли він більше не потрібен, щоб уникнути протікання потоку. (приклад)
jck

5
Зауважте, що в Java 8 є помилка, що хоч завдання виконуються у спеціальному екземплярі пулу, вони все ще поєднуються із спільним пулом: розмір обчислення залишається пропорційним загальному пулу, а не користувальницькому пулу. Виправлено на Java 10: JDK-8190974
Terran

3
@terran Ця проблема також виправлена ​​для помилок
Cutberto Ocampo

192

Паралельні потоки використовують за замовчуванням, ForkJoinPool.commonPoolякий за замовчуванням має один менший потік, як у вас процесорів , як повертається Runtime.getRuntime().availableProcessors()(Це означає, що паралельні потоки використовують усі ваші процесори, оскільки вони також використовують основний потік):

Для додатків, які потребують окремого або спеціального пулу, ForkJoinPool може бути сконструйований із заданим рівнем цільового паралелізму; за замовчуванням дорівнює кількості доступних процесорів.

Це також означає, що якщо ви вклали паралельні потоки або декілька паралельних потоків, запущених одночасно, всі вони матимуть один і той же пул. Перевага: ви ніколи не будете використовувати більше за замовчуванням (кількість доступних процесорів). Недолік: ви можете не отримати "всіх процесорів", присвоєних кожному паралельному потоку, який ви ініціюєте (якщо у вас є більше одного). (Мабуть, ви можете використовувати ManagedBlocker, щоб обійти це.)

Щоб змінити спосіб виконання паралельних потоків, ви можете будь-який

  • надішліть виконання паралельного потоку до власного ForkJoinPool: yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();або
  • ви можете змінити розмір загального пулу, використовуючи системні властивості: System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")для цільового паралелізму 20 потоків. Однак це більше не працює після підтримуваного патча https://bugs.openjdk.java.net/browse/JDK-8190974 .

Приклад останнього на моїй машині, яка має 8 процесорів. Якщо я запускаю таку програму:

long start = System.currentTimeMillis();
IntStream s = IntStream.range(0, 20);
//System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
s.parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (Exception ignore) {}
    System.out.print((System.currentTimeMillis() - start) + " ");
});

Вихід:

215 216 216 216 216 216 216 216 315 316 316 316 316 316 316 316 415 416 416 416

Таким чином, ви можете бачити, що паралельний потік обробляє 8 елементів одночасно, тобто він використовує 8 потоків. Однак якщо я коментую рядок, що коментується, вихід:

215 215 215 215 215 216 216 216 216 216 216 216 216 216 216 216 216 216 216 216

Цього разу паралельний потік використав 20 потоків, і всі 20 елементів у потоці були оброблені одночасно.


30
commonPoolМає фактично один менше availableProcessors, що призводить до повної паралельності дорівнювати , availableProcessorsтому що викликають підрахунки ниток , як один.
Марко Топольник

2
подати декларацію ForkJoinTask. Для наслідування parallel() get()потрібно:stream.parallel().forEach(soSomething)).get();
Григорій Кіслін

5
Я не впевнений, що ForkJoinPool.submit(() -> stream.forEach(...))запускатиму мої дії Stream із заданим ForkJoinPool. Я б очікував, що вся потокова дія буде виконана в ForJoinPool як ОДНА дія, але всередині все ще використовується стандартний / загальний ForkJoinPool. Де ви бачили, що ForkJoinPool.submit () буде робити те, що ви говорите?
Фредерік Лейтенбергер

@FredericLeitenberger Ви, мабуть, мали намір розмістити свій коментар під відповіддю Лукаша.
assylias

2
Я бачу, зараз stackoverflow.com/a/34930831/1520422 добре показує, що він насправді працює як оголошено. Але я все ще не розумію, як це працює. Але я добре з "це працює". Дякую!
Фредерік Лейтенбергер

39

Крім того, щоб випробувати паралельний обчислення всередині вашого forkJoinPool, ви також можете передати цей пул методу CompletableFuture.supplyAsync, як у:

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CompletableFuture<List<Integer>> primes = CompletableFuture.supplyAsync(() ->
    //parallel task here, for example
    range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList()), 
    forkJoinPool
);

22

Оригінальне рішення (встановлення загальної властивості паралелізму ForkJoinPool) більше не працює. Переглядаючи посилання в оригінальній відповіді, оновлення, яке порушує це, було перенесено на Java 8. Як уже згадувалося у пов'язаних потоках, це рішення не гарантується, що воно працює назавжди. Виходячи з цього, рішення - це forkjoinpool.submit з .get рішенням, обговореним у прийнятій відповіді. Думаю, що резервний портфель також фіксує ненадійність цього рішення.

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

Я не бачу змін паралелізму, коли я ForkJoinPool.commonPool().getParallelism()в режимі налагодження.
d-coder

Дякую. Я зробив кілька тестувань / досліджень і оновив відповідь. Схоже, оновлення змінило його, як це працює в старих версіях.
Tod Casasent

Чому я продовжую отримувати це unreported exception InterruptedException; must be caught or declared to be thrownнавіть з усіма catchвинятками в циклі.
Rocky Li

Роккі, я не бачу помилок. Знання версії Java та точна лінія допоможуть. "Перервана ексцепція" пропонує спробувати не перекрити сон у вашій версії належним чином.
Tod Casasent

13

Ми можемо змінити паралелізм за замовчуванням, використовуючи таке властивість:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

який може налаштувати використовувати більше паралелізму.


Хоча це глобальне налаштування, воно працює для збільшення
paralStream

Це працювало для мене у версії openjdk "1.8.0_222"
abbas

Та сама людина, що і вище, це не працює для мене на openjdk "11.0.6"
abbas

8

Для вимірювання фактичної кількості використовуваних ниток можна перевірити Thread.activeCount():

    Runnable r = () -> IntStream
            .range(-42, +42)
            .parallel()
            .map(i -> Thread.activeCount())
            .max()
            .ifPresent(System.out::println);

    ForkJoinPool.commonPool().submit(r).join();
    new ForkJoinPool(42).submit(r).join();

Це може створити на 4-ядерному процесорі такий вихід, як:

5 // common pool
23 // custom pool

Без .parallel()цього дає:

3 // common pool
4 // custom pool

6
Thread.activeCount () не вказує вам, які потоки обробляють ваш потік. Зобразити на Thread.currentThread (). GetName (), а потім різний (). Тоді ви зрозумієте, що не кожен потік у пулі буде використаний ... Додайте затримку до вашої обробки і всі потоки в пулі будуть використані.
keyoxy

7

До цього часу я використовував рішення, описані у відповідях на це питання. Тепер я створив невелику бібліотеку під назвою Підтримка паралельних потоків для цього:

ForkJoinPool pool = new ForkJoinPool(NR_OF_THREADS);
ParallelIntStreamSupport.range(1, 1_000_000, pool)
    .filter(PrimesPrint::isPrime)
    .collect(toList())

Але, як @PabloMatiasGomez зазначив у коментарях, є недоліки щодо механізму розщеплення паралельних потоків, що сильно залежить від розміру загального пулу. Дивіться, що паралельний потік з HashSet не працює паралельно .

Я використовую це рішення лише для того, щоб мати окремі пули для різних видів роботи, але я не можу встановити розмір загального пулу до 1, навіть якщо я не використовую його.


4

Примітка. Здається, що в JDK 10 є виправлення, що гарантує, що користувальницький пул потоків використовує очікувану кількість потоків.

Виконання паралельного потоку в користувацькому ForkJoinPool має підкорятися паралельності https://bugs.openjdk.java.net/browse/JDK-8190974


1

Я спробував користувальницький ForkJoinPool, щоб змінити розмір пулу:

private static Set<String> ThreadNameSet = new HashSet<>();
private static Callable<Long> getSum() {
    List<Long> aList = LongStream.rangeClosed(0, 10_000_000).boxed().collect(Collectors.toList());
    return () -> aList.parallelStream()
            .peek((i) -> {
                String threadName = Thread.currentThread().getName();
                ThreadNameSet.add(threadName);
            })
            .reduce(0L, Long::sum);
}

private static void testForkJoinPool() {
    final int parallelism = 10;

    ForkJoinPool forkJoinPool = null;
    Long result = 0L;
    try {
        forkJoinPool = new ForkJoinPool(parallelism);
        result = forkJoinPool.submit(getSum()).get(); //this makes it an overall blocking call

    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        if (forkJoinPool != null) {
            forkJoinPool.shutdown(); //always remember to shutdown the pool
        }
    }
    out.println(result);
    out.println(ThreadNameSet);
}

Ось висновок, який говорить, що пул використовує більше потоків, ніж за замовчуванням 4 .

50000005000000
[ForkJoinPool-1-worker-8, ForkJoinPool-1-worker-9, ForkJoinPool-1-worker-6, ForkJoinPool-1-worker-11, ForkJoinPool-1-worker-10, ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-15, ForkJoinPool-1-worker-13, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2]

Але насправді є дивацтво , коли я намагався досягти такого ж результату, використовуючи ThreadPoolExecutorнаступне:

BlockingDeque blockingDeque = new LinkedBlockingDeque(1000);
ThreadPoolExecutor fixedSizePool = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, blockingDeque, new MyThreadFactory("my-thread"));

але я провалився.

Це лише розпочнеться paralStream у новій темі, а потім все інше точно так само, що ще раз доводить, що parallelStreamволя використовуватиме ForkJoinPool для запуску дочірніх потоків.


Що може бути причиною того, що не допускати інших виконавців?
омего

@omjego Це гарне запитання, можливо, ви можете почати нове запитання та надати більше деталей для розробки своїх ідей;)
Hearen

1

Ідіть, щоб отримати AbacusUtil . Номер потоку можна вказати для паралельного потоку. Ось зразок коду:

LongStream.range(4, 1_000_000).parallel(threadNum)...

Розкриття: Я розробник AbacusUtil.


1

Якщо ви не хочете покладатися на хаки для впровадження, завжди є спосіб досягти цього, застосовуючи власні колекціонери, які поєднують mapі collectсемантику ... і ви не обмежуєтеся ForkJoinPool:

list.stream()
  .collect(parallelToList(i -> fetchFromDb(i), executor))
  .join()

На щастя, це вже зроблено тут і доступно на Maven Central: http://github.com/pivovarit/parallel-collectors

Відмова: Я написав це і несу відповідальність за це.


0

Якщо ви не заперечуєте проти використання сторонньої бібліотеки, за допомогою циклоп-реагування ви можете змішувати послідовні та паралельні потоки в межах одного трубопроводу та надавати власні ForkJoinPools. Наприклад

 ReactiveSeq.range(1, 1_000_000)
            .foldParallel(new ForkJoinPool(10),
                          s->s.filter(i->true)
                              .peek(i->System.out.println("Thread " + Thread.currentThread().getId()))
                              .max(Comparator.naturalOrder()));

Або якщо ми хотіли б продовжувати обробку в послідовному потоці

 ReactiveSeq.range(1, 1_000_000)
            .parallel(new ForkJoinPool(10),
                      s->s.filter(i->true)
                          .peek(i->System.out.println("Thread " + Thread.currentThread().getId())))
            .map(this::processSequentially)
            .forEach(System.out::println);

[Розкриття інформації Я є провідним розробником циклоп-реакції]


0

Якщо вам не потрібен користувальницький ThreadPool, але ви хочете обмежити кількість одночасних завдань, ви можете використовувати:

List<Path> paths = List.of("/path/file1.csv", "/path/file2.csv", "/path/file3.csv").stream().map(e -> Paths.get(e)).collect(toList());
List<List<Path>> partitions = Lists.partition(paths, 4); // Guava method

partitions.forEach(group -> group.parallelStream().forEach(csvFilePath -> {
       // do your processing   
}));

(Дублікат із запитанням про це заблоковано, тому, будь ласка, перенесіть мене тут)


-2

ви можете спробувати реалізувати цей ForkJoinWorkerThreadFactory та ввести його до класу Fork-Join.

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

ви можете використовувати цей конструктор пулу Fork-Join для цього.

Примітки: 1. Якщо ви користуєтесь цим, врахуйте, що на основі вашої реалізації нових потоків буде впливати планування з JVM, яке, як правило, планує потоки приєднання вилок до різних ядер (трактуються як обчислювальна нитка). 2. Планування завдань за допомогою fork-join до потоків не вплине. 3. Не справді зрозумів, як паралельний потік вибирає нитки з fork-join (не вдалося знайти належну документацію на нього), тож спробуйте скористатися іншою фабрикою потоку Naming, щоб переконатися, чи збираються нитки в паралельному потоці від customThreadFactory, який ви надаєте. 4. commonThreadPool не використовуватиме цей customThreadFactory.


Чи можете ви надати корисний приклад, який би демонстрував, як використовувати те, що ви вказали?
Дж. Мюррей
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.