Java: ExecutorService, который блокируется при представлении после определенного размера очереди

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

Если я создаю ThreadPoolExecutor следующим образом:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads, numWorkerThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(maxQueue));

Затем executor.submit(callable) бросает RejectedExecutionException, когда очередь заполняется, и все потоки уже заняты.

Что я могу сделать, чтобы сделать блок executor.submit(callable) когда очередь заполнена и все потоки заняты?

ИЗМЕНИТЬ: Я пробовал this:

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

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

EDIT: (через 5 лет после запроса вопроса)

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

Ответ 1

Я сделал то же самое. Хитрость заключается в создании BlockingQueue, где метод offer() действительно является put(). (вы можете использовать любую базу BlockingQueue, которую хотите).

public class LimitedQueue<E> extends LinkedBlockingQueue<E> 
{
    public LimitedQueue(int maxSize)
    {
        super(maxSize);
    }

    @Override
    public boolean offer(E e)
    {
        // turn offer() and add() into a blocking calls (unless interrupted)
        try {
            put(e);
            return true;
        } catch(InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

}

Обратите внимание, что это работает только для пула потоков, где corePoolSize==maxPoolSize поэтому будьте осторожны (см. комментарии).

Ответ 2

Вот как я решил это на моем конце:

(обратите внимание: это решение блокирует поток, который отправляет Callable, поэтому он предотвращает выброс RejectedExecutionException)

public class BoundedExecutor extends ThreadPoolExecutor{

    private final Semaphore semaphore;

    public BoundedExecutor(int bound) {
        super(bound, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        semaphore = new Semaphore(bound);
    }

    /**Submits task to execution pool, but blocks while number of running threads 
     * has reached the bound limit
     */
    public <T> Future<T> submitButBlockIfFull(final Callable<T> task) throws InterruptedException{

        semaphore.acquire();            
        return submit(task);                    
    }


    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);

        semaphore.release();
    }
}

Ответ 3

В настоящее время принятый ответ имеет потенциально значительную проблему - он изменяет поведение ThreadPoolExecutor.execute таким образом, что если у вас есть corePoolSize < maxPoolSize, логика ThreadPoolExecutor никогда не добавит дополнительных сотрудников за пределы ядра.

От ThreadPoolExecutor.execute(Runnable):

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

В частности, этот последний "else" блок никогда не будет удален.

Лучшей альтернативой является сделать что-то похожее на то, что OP уже делает - используйте RejectedExecutionHandler, чтобы сделать тот же put логика:

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    try {
        if (!executor.isShutdown()) {
            executor.getQueue().put(r);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RejectedExecutionException("Executor was interrupted while the task was waiting to put on work queue", e);
    }
}

Есть несколько вещей, которые следует учитывать при таком подходе, как указано в комментариях (ссылаясь на этот ответ):

  • Если corePoolSize==0, то есть условие гонки, в котором все потоки в пуле могут умереть до того, как задача будет видна.
  • Использование реализации, которая обертывает задачи очереди (не относится к ThreadPoolExecutor), приведет к проблемам, если обработчик также не обернет ее таким же образом.

Сохраняя эти соображения, это решение будет работать для большинства типичных ThreadPoolExecutors и будет корректно обрабатывать случай, когда corePoolSize < maxPoolSize.

Ответ 4

Я думаю, что это так же просто, как использовать ArrayBlockingQueue вместо a LinkedBlockingQueue.

Игнорируй меня... это совершенно неправильно. ThreadPoolExecutor вызывает Queue#offer not put, который будет иметь эффект, который вам нужен.

Вы можете расширить ThreadPoolExecutor и обеспечить реализацию execute(Runnable), которая вызывает put вместо offer.

Это не кажется вполне удовлетворительным ответом, которого я боюсь.

Ответ 5

У меня была аналогичная проблема, и я реализовал это, используя beforeExecute/afterExecute hooks from ThreadPoolExecutor:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Blocks current task execution if there is not enough resources for it.
 * Maximum task count usage controlled by maxTaskCount property.
 */
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor {

    private final ReentrantLock taskLock = new ReentrantLock();
    private final Condition unpaused = taskLock.newCondition();
    private final int maxTaskCount;

    private volatile int currentTaskCount;

    public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue, int maxTaskCount) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.maxTaskCount = maxTaskCount;
    }

    /**
     * Executes task if there is enough system resources for it. Otherwise
     * waits.
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        taskLock.lock();
        try {
            // Spin while we will not have enough capacity for this job
            while (maxTaskCount < currentTaskCount) {
                try {
                    unpaused.await();
                } catch (InterruptedException e) {
                    t.interrupt();
                }
            }
            currentTaskCount++;
        } finally {
            taskLock.unlock();
        }
    }

    /**
     * Signalling that one more task is welcome
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        taskLock.lock();
        try {
            currentTaskCount--;
            unpaused.signalAll();
        } finally {
            taskLock.unlock();
        }
    }
}

Это должно быть достаточно для вас. Btw, оригинальная реализация была основана на размере задачи, потому что одна задача могла быть больше 100 раз, чем другая, и подавать две огромные задачи - убивать ящик, но запуск одного большого и большого количества был в порядке. Если ваши интенсивные задачи ввода-вывода примерно одинакового размера, вы можете использовать этот класс, иначе просто сообщите мне, и я опубликую версию на основе размера.

P.S. Вы хотите проверить ThreadPoolExecutor javadoc. Это действительно приятное руководство пользователя от Doug Lea о том, как его можно легко настроить.

Ответ 6

Я знаю, что это старый вопрос, но имел аналогичную проблему: создание новых задач было очень быстрым, и если было слишком много OutOfMemoryError, потому что существующая задача не была выполнена достаточно быстро.

В моем случае Callables отправлены, и мне нужен результат, поэтому мне нужно сохранить все Futures, возвращенные executor.submit(). Моим решением было положить Futures в BlockingQueue с максимальным размером. Когда эта очередь заполнена, больше задач не создается до тех пор, пока некоторые не будут завершены (элементы удалены из очереди). В псевдокоде:

final ExecutorService executor = Executors.newFixedThreadPool(numWorkerThreads);
final LinkedBlockingQueue<Future> futures = new LinkedBlockingQueue<>(maxQueueSize);
try {   
    Thread taskGenerator = new Thread() {
        @Override
        public void run() {
            while (reader.hasNext) {
                Callable task = generateTask(reader.next());
                Future future = executor.submit(task);
                try {
                    // if queue is full blocks until a task
                    // is completed and hence no future tasks are submitted.
                    futures.put(compoundFuture);
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();         
                }
            }
        executor.shutdown();
        }
    }
    taskGenerator.start();

    // read from queue as long as task are being generated
    // or while Queue has elements in it
    while (taskGenerator.isAlive()
                    || !futures.isEmpty()) {
        Future compoundFuture = futures.take();
        // do something
    }
} catch (InterruptedException ex) {
    Thread.currentThread().interrupt();     
} catch (ExecutionException ex) {
    throw new MyException(ex);
} finally {
    executor.shutdownNow();
}