Как сделать блок метода ThreadPoolExecutor submit(), если он насыщен?

Я хочу создать ThreadPoolExecutor так, чтобы, когда он достиг своего максимального размера и очередь заполнена, метод submit() блокируется при попытке добавить новые задачи. Нужно ли мне реализовать пользовательский RejectedExecutionHandler для этого или существует ли существующий способ сделать это с использованием стандартной библиотеки Java?

Ответ 1

Одно из возможных решений, которое я только что нашел:

public class BoundedExecutor {
    private final Executor exec;
    private final Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }

    public void submitTask(final Runnable command)
            throws InterruptedException, RejectedExecutionException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
            throw e;
        }
    }
}

Есть ли другие решения? Я бы предпочел что-то на основе RejectedExecutionHandler, поскольку он кажется стандартным способом обработки таких ситуаций.

Ответ 2

Вы можете использовать ThreadPoolExecutor и blockingQueue:

public class ImageManager {
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(blockQueueSize);
    RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    private ExecutorService executorService =  new ThreadPoolExecutor(numOfThread, numOfThread, 
        0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler);

    private int downloadThumbnail(String fileListPath){
        executorService.submit(new yourRunnable());
    }
}

Ответ 4

Вы должны использовать CallerRunsPolicy, который выполняет отклоненную задачу в вызывающем потоке. Таким образом, он не может отправлять какие-либо новые задачи исполнителю до тех пор, пока эта задача не будет выполнена, и в этот момент будут какие-то потоки свободного пула или процесс будет повторяться.

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.CallerRunsPolicy.html

Из документов:

Отклоненные задачи

Новые задачи, представленные в выполнении метода (java.lang.Runnable), будут отклоняется, когда Исполнитель закрывается, а также когда Исполнитель использует конечные границы для максимального потоков и рабочей очереди, и насыщен. В любом случае метод execute вызывает RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) метод его RejectedExecutionHandler. 4 предопределенные политики обработчиков при условии,

  • В по умолчанию ThreadPoolExecutor.AbortPolicy обработчик запускает время выполнения ОтклоненоExecutionException после отказ.
  • В ThreadPoolExecutor.CallerRunsPolicy, поток, который вызывает само выполнение выполняет задание. Это обеспечивает простой механизм контроля обратной связи, который замедлить темпы новых задач представленный.
  • В ThreadPoolExecutor.DiscardPolicy, задача, которая не может быть выполнена, просто отброшен.
  • В ThreadPoolExecutor.DiscardOldestPolicy, если исполнитель не закрыт, задача во главе рабочей очереди сброшено, а затем повторное выполнение (который может снова потерпеть неудачу, повторяться.)

Кроме того, при вызове конструктора ThreadPoolExecutor обязательно используйте ограниченную очередь, например ArrayBlockingQueue. В противном случае ничего не будет отклонено.

Изменить: в ответ на ваш комментарий установите размер ArrayBlockingQueue равным максимальному размеру пула потоков и используйте AbortPolicy.

Изменить 2: Хорошо, я вижу, к чему вы клоните. Что об этом: переопределите метод beforeExecute(), чтобы проверить, что getActiveCount() не превышает getMaximumPoolSize(), и если это произойдет, запустите и повторите попытку?

Ответ 5

Hibernate имеет BlockPolicy, который прост и может делать то, что вы хотите:

Смотрите: Executors.java

/**
 * A handler for rejected tasks that will have the caller block until
 * space is available.
 */
public static class BlockPolicy implements RejectedExecutionHandler {

    /**
     * Creates a <tt>BlockPolicy</tt>.
     */
    public BlockPolicy() { }

    /**
     * Puts the Runnable to the blocking queue, effectively blocking
     * the delegating thread until space is available.
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            e.getQueue().put( r );
        }
        catch (InterruptedException e1) {
            log.error( "Work discarded, thread was interrupted while waiting for space to schedule: {}", r );
        }
    }
}

Ответ 6

Ответ BoundedExecutor, приведенный выше из Java Concurrency в Практике, работает только правильно, если вы используете неограниченную очередь для Исполнителя, или семафорная граница не превышает размер очереди. Семафор является разделяемым государством между подающим потоком и потоками в пуле, что позволяет насытить исполнителя, даже если размер очереди < bound <= (размер очереди + размер пула).

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

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

Ответ 7

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

Поскольку ThreadPoolExecutor использует "предложение" блокировки вместо "put", позволяет переопределить поведение "предложения" блокирующей очереди:

class BlockingQueueHack<T> extends ArrayBlockingQueue<T> {

    BlockingQueueHack(int size) {
        super(size);
    }

    public boolean offer(T task) {
        try {
            this.put(task);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    }
}

ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new BlockingQueueHack(5));

Я тестировал его и, похоже, работал. Внедрение некоторой политики тайм-аута остается как упражнение для читателя.

Ответ 8

Следующий класс обертывает вокруг ThreadPoolExecutor и использует Семафор для блокировки, тогда рабочая очередь заполнена:

public final class BlockingExecutor { 

    private final Executor executor;
    private final Semaphore semaphore;

    public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
        this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
        this.semaphore = new Semaphore(queueSize + maxPoolSize);
    }

    private void execImpl (final Runnable command) throws InterruptedException {
        semaphore.acquire();
        try {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            // will never be thrown with an unbounded buffer (LinkedBlockingQueue)
            semaphore.release();
            throw e;
        }
    }

    public void execute (Runnable command) throws InterruptedException {
        execImpl(command);
    }
}

Этот класс-оболочка основан на решении, приведенном в книге Java Concurrency на практике Брайаном Гетцем. Решение в книге принимает только два параметра конструктора: a Executor и привязка, используемая для семафора. Это показано в ответе, заданном Fixpoint. Существует проблема с этим подходом: он может попасть в состояние, когда потоки пулов заняты, очередь заполнена, но семафор только что выпустил разрешение. (semaphore.release() в блоке finally). В этом состоянии новая задача может захватить только что выпущенное разрешение, но отклоняется, потому что очередь задач заполнена. Конечно, это не то, чего вы хотите; вы хотите заблокировать в этом случае.

Чтобы решить эту проблему, мы должны использовать неограниченную очередь, как ясно указывает JCiP. Семафор действует как охранник, дающий эффект размера виртуальной очереди. Это имеет побочный эффект, что возможно, что устройство может содержать задачи maxPoolSize + virtualQueueSize + maxPoolSize. Почему это? Из-за semaphore.release() в блоке finally. Если все потоки пулов одновременно вызывают этот оператор, то освобождаются разрешения maxPoolSize, позволяя тому же количеству задач вводить устройство. Если бы мы использовали ограниченную очередь, она все равно была бы полной, что привело бы к отклоненной задаче. Теперь, поскольку мы знаем, что это происходит только тогда, когда поток потока почти завершен, это не проблема. Мы знаем, что поток пула не будет блокироваться, поэтому в скором времени задача будет взята из очереди.

Однако вы можете использовать ограниченную очередь. Просто убедитесь, что его размер равен virtualQueueSize + maxPoolSize. Большие размеры бесполезны, семафор предотвратит возможность добавления большего количества элементов. Меньшие размеры приведут к отклоненным задачам. Вероятность отклонения заданий возрастает по мере уменьшения размера. Например, скажем, что вы хотите ограниченный исполнитель с maxPoolSize = 2 и virtualQueueSize = 5. Затем возьмите семафор с разрешениями 5 + 2 = 7 и фактическим размером очереди 5 + 2 = 7. Реальное количество задач, которые могут быть в устройстве, равно 2 + 5 + 2 = 9. Когда исполнитель заполнен (5 задач в очереди, 2 в пуле потоков, поэтому разрешено 0), а ВСЕ потоки пулов выпускают свои разрешения, тогда только 2 разрешения могут выполняться при выполнении задач.

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

Ответ 9

вы можете использовать пользовательский RejectedExecutionHandler, как этот

ThreadPoolExecutor tp= new ThreadPoolExecutor(core_size, // core size
                max_handlers, // max size 
                timeout_in_seconds, // idle timeout 
                TimeUnit.SECONDS, queue, new RejectedExecutionHandler() {
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        // This will block if the queue is full
                        try {
                            executor.getQueue().put(r);
                        } catch (InterruptedException e) {
                            System.err.println(e.getMessage());
                        }

                    }
                });

Ответ 10

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

Я верю, что это заставит вас задерживать поведение, которое вы ищете. Обработчик отклонения никогда не будет соответствовать счету, поскольку это указывает, что исполнитель не может выполнить задачу. То, что я мог себе представить, заключается в том, что вы получаете какую-то форму "занятого ожидания" в обработчике. Это не то, что вы хотите, вам нужна очередь для исполнителя, которая блокирует вызывающего абонента...

Ответ 12

Чтобы избежать проблем с решением @FixPoint. Можно использовать ListeningExecutorService и выпустить семафор onSuccess и onFailure внутри FutureCallback.

Ответ 13

Недавно я обнаружил, что этот вопрос имеет ту же проблему. OP не говорит об этом явно, но мы не хотим использовать RejectedExecutionHandler, который выполняет задачу в потоке отправителя, потому что это будет недоиспользовать рабочие потоки, если эта задача является длинной.

Считывая все ответы и комментарии, в частности, ошибочное решение с помощью семафора или используя afterExecute, я более подробно рассмотрел код ThreadPoolExecutor, чтобы узнать, есть ли выход. Я был поражен, увидев, что существует более 2000 строк кода (прокомментированного), некоторые из которых заставляют меня чувствовать головокружение. Учитывая довольно простое требование, на самом деле у меня есть --- один производитель, несколько потребителей, пусть производитель блокирует, когда потребители не могут работать, - я решил бросить свое решение. Это не ExecutorService, а просто Executor. И он не адаптирует количество потоков к рабочей нагрузке, но содержит только фиксированное количество потоков, что также соответствует моим требованиям. Вот код. Не стесняйтесь говорить об этом: -)

package x;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;

/**
 * distributes {@code Runnable}s to a fixed number of threads. To keep the
 * code lean, this is not an {@code ExecutorService}. In particular there is
 * only very simple support to shut this executor down.
 */
public class ParallelExecutor implements Executor {
  // other bounded queues work as well and are useful to buffer peak loads
  private final BlockingQueue<Runnable> workQueue =
      new SynchronousQueue<Runnable>();
  private final Thread[] threads;

  /*+**********************************************************************/
  /**
   * creates the requested number of threads and starts them to wait for
   * incoming work
   */
  public ParallelExecutor(int numThreads) {
    this.threads = new Thread[numThreads];
    for(int i=0; i<numThreads; i++) {
      // could reuse the same Runner all over, but keep it simple
      Thread t = new Thread(new Runner());
      this.threads[i] = t;
      t.start();
    }
  }
  /*+**********************************************************************/
  /**
   * returns immediately without waiting for the task to be finished, but may
   * block if all worker threads are busy.
   * 
   * @throws RejectedExecutionException if we got interrupted while waiting
   *         for a free worker
   */
  @Override
  public void execute(Runnable task)  {
    try {
      workQueue.put(task);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RejectedExecutionException("interrupt while waiting for a free "
          + "worker.", e);
    }
  }
  /*+**********************************************************************/
  /**
   * Interrupts all workers and joins them. Tasks susceptible to an interrupt
   * will preempt their work. Blocks until the last thread surrendered.
   */
  public void interruptAndJoinAll() throws InterruptedException {
    for(Thread t : threads) {
      t.interrupt();
    }
    for(Thread t : threads) {
      t.join();
    }
  }
  /*+**********************************************************************/
  private final class Runner implements Runnable {
    @Override
    public void run() {
      while (!Thread.currentThread().isInterrupted()) {
        Runnable task;
        try {
          task = workQueue.take();
        } catch (InterruptedException e) {
          // canonical handling despite exiting right away
          Thread.currentThread().interrupt(); 
          return;
        }
        try {
          task.run();
        } catch (RuntimeException e) {
          // production code to use a logging framework
          e.printStackTrace();
        }
      }
    }
  }
}

Ответ 14

Я считаю, что существует довольно элегантный способ решить эту проблему, используя java.util.concurrent.Semaphore и делегируя поведение Executor.newFixedThreadPool. Новая служба-исполнитель выполнит только новую задачу, когда есть поток для этого. Блокировка управляется с помощью Semaphore с количеством разрешений, равным количеству потоков. Когда задача завершена, она возвращает разрешение.

public class FixedThreadBlockingExecutorService extends AbstractExecutorService {

private final ExecutorService executor;
private final Semaphore blockExecution;

public FixedThreadBlockingExecutorService(int nTreads) {
    this.executor = Executors.newFixedThreadPool(nTreads);
    blockExecution = new Semaphore(nTreads);
}

@Override
public void shutdown() {
    executor.shutdown();
}

@Override
public List<Runnable> shutdownNow() {
    return executor.shutdownNow();
}

@Override
public boolean isShutdown() {
    return executor.isShutdown();
}

@Override
public boolean isTerminated() {
    return executor.isTerminated();
}

@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    return executor.awaitTermination(timeout, unit);
}

@Override
public void execute(Runnable command) {
    blockExecution.acquireUninterruptibly();
    executor.execute(() -> {
        try {
            command.run();
        } finally {
            blockExecution.release();
        }
    });
}

Ответ 15

У меня была такая же потребность в прошлом: какая-то блокирующая очередь с фиксированным размером для каждого клиента, поддерживаемая общим пулом потоков. Я закончил писать свой собственный ThreadPoolExecutor:

UserThreadPoolExecutor  (блокировка очереди (для каждого клиента) + threadpool (совместно используемая среди всех клиентов))

Смотрите: https://github.com/d4rxh4wx/UserThreadPoolExecutor

Каждому UserThreadPoolExecutor предоставляется максимальное количество потоков из общего потока ThreadPoolExecutor

Каждый пользователь UserThreadPoolExecutor может:

  • отправить задание в общий исполнитель пула потоков, если его квота не достигнута. Если достигнута квота, задание будет поставлено в очередь (блокировка, не требующая потребления, ожидающая процессора). Как только одна из поставленных задач будет завершена, квота будет уменьшена, что позволит другой задаче, ожидающей отправки в ThreadPoolExecutor
  • дождитесь завершения оставшихся задач

Ответ 16

Я нашел эту политику отклонения в эластичном клиенте поиска. Он блокирует поток вызывающего абонента при блокировке очереди. Код ниже

 static class ForceQueuePolicy implements XRejectedExecutionHandler 
 {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
        {
            try 
            {
                executor.getQueue().put(r);
            } 
            catch (InterruptedException e) 
            {
                //should never happen since we never wait
                throw new EsRejectedExecutionException(e);
            }
        }

        @Override
        public long rejected() 
        {
            return 0;
        }
}

Ответ 17

Недавно мне нужно было добиться чего-то подобного, но на ScheduledExecutorService.

Мне также нужно было убедиться, что я обрабатываю задержку, передаваемую методу, и гарантирую, что либо задача будет отправлена ​​для выполнения в то время, когда вызывающий абонент ожидает, либо просто завершит сбой, тем самым выбросив RejectedExecutionException.

Другие методы из ScheduledThreadPoolExecutor для выполнения или отправки внутренней задачи вызова #schedule, которая все равно будет вызывать переопределенные методы.

import java.util.concurrent.*;

public class BlockingScheduler extends ScheduledThreadPoolExecutor {
    private final Semaphore maxQueueSize;

    public BlockingScheduler(int corePoolSize,
                             ThreadFactory threadFactory,
                             int maxQueueSize) {
        super(corePoolSize, threadFactory, new AbortPolicy());
        this.maxQueueSize = new Semaphore(maxQueueSize);
    }

    @Override
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(delay));
        return super.schedule(command, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(callable, unit.toMillis(delay));
        return super.schedule(callable, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleAtFixedRate(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long period,
                                                     TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleWithFixedDelay(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable t) {
        super.afterExecute(runnable, t);
        try {
            if (t == null && runnable instanceof Future<?>) {
                try {
                    ((Future<?>) runnable).get();
                } catch (CancellationException | ExecutionException e) {
                    t = e;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null) {
                System.err.println(t);
            }
        } finally {
            releaseQueueUsage();
        }
    }

    private long beforeSchedule(Runnable runnable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(runnable, this);
            return 0;
        }
    }

    private long beforeSchedule(Callable callable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(new FutureTask(callable), this);
            return 0;
        }
    }

    private long getQueuePermitAndModifiedDelay(long delay) throws InterruptedException {
        final long beforeAcquireTimeStamp = System.currentTimeMillis();
        maxQueueSize.tryAcquire(delay, TimeUnit.MILLISECONDS);
        final long afterAcquireTimeStamp = System.currentTimeMillis();
        return afterAcquireTimeStamp - beforeAcquireTimeStamp;
    }

    private void releaseQueueUsage() {
        maxQueueSize.release();
    }
}

У меня есть код здесь, оценят любую обратную связь. https://github.com/AmitabhAwasthi/BlockingScheduler

Ответ 18

Вот решение, которое, кажется, работает очень хорошо. Он назывался NotifyingBlockingThreadPoolExecutor.

Демо-программа.

Изменить: с этим кодом существует issue, метод await() ошибочен. Вызов shutdown() + awaitTermination() работает нормально.

Ответ 19

Мне не всегда нравится CallerRunsPolicy, тем более, что он позволяет отклоненной задаче "пропустить очередь" и выполнять выполнение перед задачами, которые были отправлены ранее. Более того, выполнение задачи в вызывающем потоке может занять гораздо больше времени, чем ждать, пока первый слот станет доступным.

Я решил эту проблему, используя пользовательский 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);
    }
}

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

executorPool = new ThreadPoolExecutor(1, 1, 10,
                                      TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
                                      new BlockWhenQueueFull());

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

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