Java:在某个队列大小之后阻止提交的ExecutorService

时间:2010-12-23 19:50:46

标签: java concurrency threadpool executorservice

我正在尝试编写一个解决方案,其中单个线程生成可以并行执行的I / O密集型任务。每个任务都有重要的内存数据。所以我希望能够限制暂时待处理的任务数量。

如果我像这样创建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());

它有点实现了我想要实现的效果,但是以一种不雅的方式(基本上被拒绝的线程在调用线程中运行,因此这会阻止调用线程提交更多)。

编辑:(提问后5年)

对于阅读此问题及其答案的任何人,请不要将接受的答案作为一个正确的解决方案。请仔细阅读所有答案和评论。

8 个答案:

答案 0 :(得分:60)

我做了同样的事情。诀窍是创建一个BlockingQueue,其中offer()方法实际上是put()。 (你可以使用你想要的任何基础BlockingQueue impl。)。

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的线程池,因此请注意(请参阅注释)。

答案 1 :(得分:15)

以下是我在最后解决这个问题的方法:

(注意:此解决方案会阻止提交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();
    }
}

答案 2 :(得分:10)

当前接受的答案有一个潜在的重大问题 - 它改变了ThreadPoolExecutor.execute的行为,这样如果你有corePoolSize < maxPoolSize,ThreadPoolExecutor逻辑将永远不会在核心之外添加额外的工作者。

ThreadPoolExecutor。执行(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);

具体来说,最后一个&#39;否则&#39;阻止willl永远不会被击中。

更好的选择是做类似于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);
    }
}

正如评论中所指出的(参考this answer),这种方法有一些值得注意的事项:

  1. 如果corePoolSize==0,则存在竞争条件,其中池中的所有线程可能在任务可见之前死亡
  2. 使用包装队列任务的实现(不适用于ThreadPoolExecutor)将导致问题,除非处理程序也以相同的方式包装它。
  3. 记住这些问题,此解决方案适用于大多数典型的ThreadPoolExecutors,并将正确处理corePoolSize < maxPoolSize的情况。

答案 3 :(得分:4)

如果您使用的是 spring-integration,如何使用 CallerBlocksPolicy 类?

该类实现了 RejectedExecutionHandler 接口,该接口是用于处理无法由 ThreadPoolExecutor 执行的任务的处理程序。

您可以像这样使用此政策。

executor.setRejectedExecutionHandler(new CallerBlocksPolicy());

CallerBlocksPolicyCallerRunsPolicy 的主要区别在于它是在调用者线程中阻塞还是运行任务。

请参阅this code

答案 4 :(得分:2)

我遇到了类似的问题,我使用beforeExecute/afterExecute中的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();
        }
    }
}

这对你来说应该足够好了。顺便说一下,最初的实现是基于任务大小的,因为一个任务可能比另一个任务大100倍,并且提交两个巨大的任务就是杀死了这个盒子,但是运行一个大而且大量的小就是好的。如果您的I / O密集型任务的大小大致相同,则可以使用此类,否则只需告诉我,我将发布基于大小的实现。

P.S。您需要检查ThreadPoolExecutor javadoc。 Doug Lea关于如何轻松定制它是非常好的用户指南。

答案 5 :(得分:2)

我知道这是一个老问题,但有一个类似的问题,即创建新任务的速度非常快,如果出现太多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();
}

答案 6 :(得分:1)

我认为这就像使用ArrayBlockingQueue而不是LinkedBlockingQueue一样简单。

忽略我......那是完全错误的。 ThreadPoolExecutor调用Queue#offer而非put,这会产生您需要的效果。

您可以扩展ThreadPoolExecutor并提供execute(Runnable)的实施,以put代替offer

我担心这似乎不是一个完全令人满意的答案。

答案 7 :(得分:1)

我已经在装饰器模式之后实现了一个解决方案,并使用信号量来控制执行任务的数量。您可以将其与任何Executor和:

一起使用
  • 指定正在进行的任务的最大值
  • 指定等待任务执行许可的最大超时时间(如果超时通过且未获取许可,则抛出RejectedExecutionException
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;

import javax.annotation.Nonnull;

public class BlockingOnFullQueueExecutorDecorator implements Executor {

    private static final class PermitReleasingDecorator implements Runnable {

        @Nonnull
        private final Runnable delegate;

        @Nonnull
        private final Semaphore semaphore;

        private PermitReleasingDecorator(@Nonnull final Runnable task, @Nonnull final Semaphore semaphoreToRelease) {
            this.delegate = task;
            this.semaphore = semaphoreToRelease;
        }

        @Override
        public void run() {
            try {
                this.delegate.run();
            }
            finally {
                // however execution goes, release permit for next task
                this.semaphore.release();
            }
        }

        @Override
        public final String toString() {
            return String.format("%s[delegate='%s']", getClass().getSimpleName(), this.delegate);
        }
    }

    @Nonnull
    private final Semaphore taskLimit;

    @Nonnull
    private final Duration timeout;

    @Nonnull
    private final Executor delegate;

    public BlockingOnFullQueueExecutorDecorator(@Nonnull final Executor executor, final int maximumTaskNumber, @Nonnull final Duration maximumTimeout) {
        this.delegate = Objects.requireNonNull(executor, "'executor' must not be null");
        if (maximumTaskNumber < 1) {
            throw new IllegalArgumentException(String.format("At least one task must be permitted, not '%d'", maximumTaskNumber));
        }
        this.timeout = Objects.requireNonNull(maximumTimeout, "'maximumTimeout' must not be null");
        if (this.timeout.isNegative()) {
            throw new IllegalArgumentException("'maximumTimeout' must not be negative");
        }
        this.taskLimit = new Semaphore(maximumTaskNumber);
    }

    @Override
    public final void execute(final Runnable command) {
        Objects.requireNonNull(command, "'command' must not be null");
        try {
            // attempt to acquire permit for task execution
            if (!this.taskLimit.tryAcquire(this.timeout.toMillis(), MILLISECONDS)) {
                throw new RejectedExecutionException(String.format("Executor '%s' busy", this.delegate));
            }
        }
        catch (final InterruptedException e) {
            // restore interrupt status
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }

        this.delegate.execute(new PermitReleasingDecorator(command, this.taskLimit));
    }

    @Override
    public final String toString() {
        return String.format("%s[availablePermits='%s',timeout='%s',delegate='%s']", getClass().getSimpleName(), this.taskLimit.availablePermits(),
                this.timeout, this.delegate);
    }
}