Java ThreadPoolExecutor: Оновлення розміру основного пулу динамічно відхиляє вхідні завдання з перервами


13

У мене виникає проблема, коли, якщо я спробую змінити розмір ThreadPoolExecutorосновного пулу до іншого числа після створення пулу, то періодично деякі завдання відхиляються, RejectedExecutionExceptionнавіть якщо я ніколи не подаю більше, ніж queueSize + maxPoolSizeкількість завдань.

Проблема, яку я намагаюся вирішити, полягає в тому, щоб розширити ThreadPoolExecutorрозмір основних потоків на основі очікуваних виконань, що знаходяться в черзі пулу потоків. Мені це потрібно, тому що за замовчуванням a ThreadPoolExecutorстворить нове, Threadлише якщо черга заповнена.

Ось невеличка автономна програма Pure Java 8, яка демонструє проблему.

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolResizeTest {

    public static void main(String[] args) throws Exception {
        // increase the number of iterations if unable to reproduce
        // for me 100 iterations have been enough
        int numberOfExecutions = 100;

        for (int i = 1; i <= numberOfExecutions; i++) {
            executeOnce();
        }
    }

    private static void executeOnce() throws Exception {
        int minThreads = 1;
        int maxThreads = 5;
        int queueCapacity = 10;

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                minThreads, maxThreads,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity),
                new ThreadPoolExecutor.AbortPolicy()
        );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
                0, 10, TimeUnit.MILLISECONDS);
        CompletableFuture<Void> taskBlocker = new CompletableFuture<>();

        try {
            int totalTasksToSubmit = queueCapacity + maxThreads;

            for (int i = 1; i <= totalTasksToSubmit; i++) {
                // following line sometimes throws a RejectedExecutionException
                pool.submit(() -> {
                    // block the thread and prevent it from completing the task
                    taskBlocker.join();
                });
                // Thread.sleep(10); //enabling even a small sleep makes the problem go away
            }
        } finally {
            taskBlocker.complete(null);
            scheduler.shutdown();
            pool.shutdown();
        }
    }

    /**
     * Resize the thread pool if the number of pending tasks are non-zero.
     */
    private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
        int pendingExecutions = pool.getQueue().size();
        int approximateRunningExecutions = pool.getActiveCount();

        /*
         * New core thread count should be the sum of pending and currently executing tasks
         * with an upper bound of maxThreads and a lower bound of minThreads.
         */
        int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));

        pool.setCorePoolSize(newThreadCount);
        pool.prestartAllCoreThreads();
    }
}

Чому пул повинен коли-небудь кидати RejectedExecutionException, якщо я ніколи не надсилаю більше, ніж queueCapacity + maxThreads. Я ніколи не змінюю максимальні нитки, тому за визначенням ThreadPoolExecutor, воно повинно або вміщувати завдання в тему, або в чергу.

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

Будь-які вказівки про те, як виправити RejectedExecutionException?


Чому б не забезпечити власну реалізацію ExecutorService, обернувши наявну, яка повторно подає завдання, які не вдалося подати через зміни розміру?
daniu

@daniu - це обхідний шлях. Суть питань полягає в тому, чому пул повинен коли-небудь кидати RejectedExecutionException, якщо я ніколи не надсилаю більше, ніж queueCapacity + maxThreads. Я ніколи не змінюю максимальні нитки, тому за визначенням ThreadPoolExecutor, воно повинно або вміщувати завдання в тему, або в чергу.
Swaranga Sarma

Гаразд, я, здається, неправильно зрозумів ваше запитання. Що це? Ви хочете знати, чому відбувається поведінка або як ви її оточуєте, викликаючи у вас проблеми?
daniu

Так, змінити мою реалізацію на службу виконавця неможливо, оскільки багато коду відноситься до ThreadPoolExecutor. Тож якщо я все-таки хотів мати змінний ThreadPoolExecutor, мені потрібно знати, як я можу це виправити. Можливо, правильний спосіб зробити щось подібне - розширити ThreadPoolExecutor і отримати доступ до деяких захищених змінних та оновити розмір пулу в синхронізованому блоці на блокування, яким поділяється суперклас.
Swaranga Sarma

Розширення ThreadPoolExecutor- це, мабуть, погана ідея, і чи не потрібно вам змінювати існуючий код і в цьому випадку? Було б найкраще надати приклад того, як ваш фактичний код звертається до виконавця. Я був би здивований, якби він використовував багато методів, специфічних для ThreadPoolExecutor(тобто не для ExecutorService).
daniu

Відповіді:


5

Ось сценарій, чому це відбувається:

У своєму прикладі я використовую minThreads = 0, maxThreads = 2 та queueCapacity = 2, щоб скоротити її. Перша команда надходить, це робиться в методі Execute:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

для цієї команди виконується workQueue.offer (команда), ніж addWorker (null, false). Робочий потік спочатку виймає цю команду з черги методом запуску потоку, тому в цей час у черзі все ще є одна команда,

Друга команда отримує цього разу представлену workQueue.offer (команду). Зараз черга заповнена

Тепер ScheduledExecutorService виконує метод resizeThreadPool, який викликає setCorePoolSize з maxThreads. Ось метод setCorePoolSize:

 public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

Цей метод додає одного працівника за допомогою addWorker (null, true). Ні, працює 2 черги на робочі, максимум і черга заповнена.

Третя команда надсилається та виходить з ладу, тому що workQueue.offer (команда) та addWorker (команда, помилка) не вдається, що призводить до винятку:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@24c22fe rejected from java.util.concurrent.ThreadPoolExecutor@cd1e646[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at ThreadPoolResizeTest.executeOnce(ThreadPoolResizeTest.java:60)
at ThreadPoolResizeTest.runTest(ThreadPoolResizeTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:365)

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


Правильно. Мені вдалося спростувати, скопіювавши код у свій клас та додавши реєстратори. В основному, коли черга заповнена, і я надсилаю нове завдання, вона спробує створити нового Worker. Тим часом, якщо в цей момент мій резизер також викликає setCorePoolSize до 2, це також створює нового Worker. На даний момент два працівники змагаються за додавання, але обох вони не можуть бути, оскільки це порушить обмеження максимального розміру пулу, тому подання нового завдання буде відхилено. Я думаю, що це умова гонки, і я подав звіт про помилку в OpenJDK. Подивимось. Але ти відповів на моє запитання, щоб ти отримав щедрість. Дякую.
Swaranga Sarma

2

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

Документи Java

Завод нових тем. Усі потоки створені за допомогою цієї фабрики (за допомогою методу addWorker). Усі абоненти повинні бути готові до відмови addWorker, що може відображати систему або політику користувача, що обмежує кількість потоків. Незважаючи на те, що це не трактується як помилка, невдача у створенні потоків може призвести до відхилення нових завдань, а існуючих залишаються застряглими у черзі.

Коли ви зміните розмір основного пулу, скажімо, збільшується, створюються додаткові працівники ( addWorkerметод в setCorePoolSize), а виклик для створення додаткової роботи ( addWorkerметод від execute) відхиляється, коли addWorkerповертається помилковим ( add Workerостанній фрагмент коду), оскільки вже достатньо додаткових працівників створено, setCorePoolSize але ще не запущено для відображення оновлення у черзі .

Відповідні частини

Порівняйте

public void setCorePoolSize(int corePoolSize) {
    ....
    int k = Math.min(delta, workQueue.size());
    while (k-- > 0 && addWorker(null, true)) {
        if (workQueue.isEmpty())
             break;
    }
}

public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

private boolean addWorker(Runnable firstTask, boolean core) {
....
   if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
     return false;             
}

Використовуйте спеціальний обробник виконання спроб відхилення (Це повинно працювати для вашого випадку, оскільки у вас є верхня межа як максимальний розмір пулу). Будь ласка, відрегулюйте за потребою.

public static class RetryRejectionPolicy implements RejectedExecutionHandler {
    public RetryRejectionPolicy () {}

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
           while(true)
            if(e.getQueue().offer(r)) break;
        }
    }
}

ThreadPoolExecutor pool = new ThreadPoolExecutor(
      minThreads, maxThreads,
      0, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(queueCapacity),
      new ThreadPoolResizeTest.RetryRejectionPolicy()
 );

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


Я думаю, що завершення роботи очікує на вже подані завдання, відповідно до JavaDoc: shutdown () Ініціює впорядковане завершення роботи, в якому виконуються раніше подані завдання, але нові завдання не приймаються.
Томас Крігер

@ThomasKrieger - Він виконає вже подані завдання, але не чекатиме їх закінчення - від docs docs.oracle.com/javase/7/docs/api/java/util/concurrent/… - Цей метод не чекає надісланого раніше завдання на повне виконання. Для цього використовуйте awaitTermation.
Сагар Веерам
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.