Блок ThreadPoolExecutor, когда очередь заполнена?

Я пытаюсь выполнить множество задач с помощью ThreadPoolExecutor. Ниже приведен гипотетический пример:

def workQueue = new ArrayBlockingQueue<Runnable>(3, false)
def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue)
for(int i = 0; i < 100000; i++)
    threadPoolExecutor.execute(runnable)

Проблема в том, что я быстро получил java.util.concurrent.RejectedExecutionException, поскольку количество задач превышает размер рабочей очереди. Тем не менее, желаемое поведение, которое я ищу, состоит в том, чтобы иметь блок основного потока, пока в очереди не будет места. Каков наилучший способ сделать это?

Ответ 1

В некоторых очень узких условиях вы можете реализовать java.util.concurrent.RejectedExecutionHandler, который делает то, что вам нужно.

RejectedExecutionHandler block = new RejectedExecutionHandler() {
  rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
     executor.getQueue().put( r );
  }
};

ThreadPoolExecutor pool = new ...
pool.setRejectedExecutionHandler(block);

Теперь. Это очень плохая идея по следующим причинам.

  • Он подвержен тупиковой ситуации, потому что все потоки в пуле могут умереть, прежде чем вещь, которую вы положили в очередь, видна. Смягчите это, установив разумное время сохранения.
  • Задача не завершена так, как может ожидать ваш Executor. Множество реализаций исполнителей завершают свои задачи в каком-то объекте отслеживания перед выполнением. Посмотрите на свой источник.
  • Добавление через getQueue() сильно не рекомендуется API и в какой-то момент может быть запрещено.

Стратегия почти всегда лучше - установить ThreadPoolExecutor.CallerRunsPolicy, которая будет дросселировать ваше приложение, запустив задачу в потоке, который вызывает execute().

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

  • У вас есть только один поток, вызывающий execute()
  • Вы должны (или хотите) иметь очень маленькую длину очереди
  • Вам абсолютно необходимо ограничить количество потоков, выполняющих эту работу (как правило, по внешним причинам), и стратегия caller-run нарушит это.
  • Ваши задачи имеют непредсказуемый размер, поэтому вызовы-вызовы могут вводить голод, если пул на мгновение занят четырьмя короткими задачами, а ваш один поток, выполняющий вызов, застрял с большим.

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

Удачи.

Ответ 2

Проверьте ответы на контрольные точки здесь: fooobar.com/questions/64430/..., взятые из книги "Java Concurrency на практике".

Ответ 3

Вот мой фрагмент кода в этом случае:

public void executeBlocking( Runnable command ) {
    if ( threadPool == null ) {
        logger.error( "Thread pool '{}' not initialized.", threadPoolName );
        return;
    }
    ThreadPool threadPoolMonitor = this;
    boolean accepted = false;
    do {
        try {
            threadPool.execute( new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    }
                    // to make sure that the monitor is freed on exit
                    finally {
                        // Notify all the threads waiting for the resource, if any.
                        synchronized ( threadPoolMonitor ) {
                            threadPoolMonitor.notifyAll();
                        }
                    }
                }
            } );
            accepted = true;
        }
        catch ( RejectedExecutionException e ) {
            // Thread pool is full
            try {
                // Block until one of the threads finishes its job and exits.
                synchronized ( threadPoolMonitor ) {
                    threadPoolMonitor.wait();
                }
            }
            catch ( InterruptedException ignored ) {
                // return immediately
                break;
            }
        }
    } while ( !accepted );
}

threadPool - это локальный экземпляр java.util.concurrent.ExecutorService, который уже был инициализирован.

Ответ 4

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

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

Этот класс можно просто использовать в исполнителе потока-пула как RejectedExecutionHandler, как и любой другой. В этом примере:

executorPool = new def threadPoolExecutor = new ThreadPoolExecutor(3, 3, 1L, TimeUnit.HOURS, workQueue, new BlockWhenQueueFull())

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

Тем не менее, мне лично нравится этот метод. Он компактный, понятный и хорошо работает. Я пропустил что-то важное?