Java:ExecutorService在特定队列大小后会在提交时阻止


82

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

如果我这样创建ThreadPoolExecutor:

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

然后,当队列已满并且所有线程都已经繁忙时,executor.submit(callable)抛出该异常RejectedExecutionException

executor.submit(callable)当队列已满并且所有线程都忙时,我该怎么做才能阻塞?

编辑:我试过

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

它以某种微妙的方式达到了我想要达到的效果(基本上被拒绝的线程在调用线程中运行,因此这阻止了调用线程提交更多信息)。

编辑:(问了问题5年后)

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



1
我之前曾使用过信号量来做到这一点,就像在对与@axtavt链接的非常相似的问题的回答中一样。
斯蒂芬·丹尼

2
@TomWolk一方面,与numWorkerThreads调用者线程也正在执行任务相比,您可以并行执行一个任务。但是,更重要的问题是,如果调用者线程获得了长时间运行的任务,则其他线程可能会闲置以等待下一个任务。
塔希尔·阿赫塔尔

2
@TahirAkhtar,是的;队列应该足够长,以便在调用者必须自己执行任务时不会耗尽。但是我认为,如果可以使用另一个线程(调用者线程)来执行任务,则是一个优势。如果调用方只是阻塞,则调用方的线程将处于空闲状态。我将CallerRunsPolicy与线程池的容量的三倍一起使用,并且工作流畅。与该解决方案相比,我将考虑对框架进行过度设计。
TomWolk '16

1
@TomWalk +1好点。似乎另一个区别是,如果任务从队列中被拒绝并由调用者线程运行,则调用者线程将开始处理请求混乱,因为它没有等待轮到队列。当然,如果您已经选择使用线程,则必须正确处理任何依赖关系,但要记住一点。
rimsky

Answers:


63

我也做过同样的事情。诀窍是创建一个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
或者,您可以扩展SynchronousQueue以防止缓冲,仅允许直接切换。
brendon

优雅,直接解决问题。offer()变成put(),而put()表示“ ...在必要时等待空间可用”
特伦顿

5
我认为这不是一个好主意,因为它更改了offer方法的协议。Offer方法应为非阻塞调用。
Mingjiang Shi

6
我不同意-这会改变ThreadPoolExecutor.execute的行为,以使如果您具有corePoolSize <maxPoolSize,则ThreadPoolExecutor逻辑将永远不会在核心之外添加其他工作线程。
Krease

5
需要澄清的是,您的解决方案仅在您维持约束where时才有效corePoolSize==maxPoolSize。没有它,它不再让ThreadPoolExecutor具有设计的行为。我一直在寻找解决这个问题的解决方案,但没有限制。有关我们最终采用的方法,请参见下面的替代答案。
Krease

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

1
我认为这在corePoolSize < maxPoolSize...:|的情况下效果不佳
rogerdpack

1
它适用于的情况corePoolSize < maxPoolSize。在这种情况下,信号灯将可用,但不会有线程,并且SynchronousQueue将返回false。然后,ThreadPoolExecutor它将旋转一个新线程。该解决方案的问题在于它具有竞争条件。之后semaphore.release(),但在线程完成之前execute,submit()将获得信号灯许可。如果super.submit()在execute()完成之前运行,则该作业将被拒绝。
路易斯·吉列尔梅

@LuísGuilherme但是在线程完成执行之前永远不会调用semaphore.release()。因为此调用是在after Execute(...)方法中完成的。我在您描述的场景中缺少什么吗?
cvacca

1
afterExecute由运行任务的同一线程调用,因此尚未完成。自己做测试。实施该解决方案,并在执行器上投入大量工作,如果工作被拒绝,则会抛出该工作。您会注意到,是的,这是一个竞争条件,复制它并不难。
路易斯·吉列尔梅

1
转到ThreadPoolExecutor并检查runWorker(Worker w)方法。您将看到在afterExecute完成后发生的事情,包括解锁工作程序和增加已完成任务的数量。因此,您允许任务进入(通过释放信号量)而不必用带宽来处理它们(通过调用processWorkerExit)。
路易斯·吉列尔梅

14

当前接受的答案有一个潜在的重大问题-它更改了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);
    }
}

正如评论中所指出的(请参考此答案),使用这种方法需要注意一些事项:

  1. 如果为corePoolSize==0,则存在争用条件,其中池中的所有线程可能在可见任务之前死亡
  2. 使用包装队列任务的实现(不适用于ThreadPoolExecutor)将导致问题,除非处理程序也以相同的方式包装它。

牢记这些陷阱,此解决方案适用于大多数典型的ThreadPoolExecutor,并且可以正确处理where的情况corePoolSize < maxPoolSize


对于那些不满意的人-您能提供一些见解吗?这个答案有什么不正确/误导/危险的地方吗?我想有机会解决您的问题。
Krease

2
我没有投票,但这似乎是一个非常糟糕的主意
vanOekel 2015年

@vanOekel-谢谢您的链接-该答案提出了一些有效的案例,如果使用这种方法,应该知道这些案例,但是IMO并没有将其视为“非常不好的主意”-它仍然解决了当前接受的答案中存在的问题。我已经用这些警告更新了我的答案。
Krease

如果核心池大小为0,并且如果任务已提交给执行程序,则如果队列已满,则执行程序将开始创建线程以处理任务。那么为什么它容易陷入僵局。没明白你的意思。您能详细说明吗?
Shirgill Farhan

@ShirgillFarhanAnsari-这是先前评论中提到的情况。发生这种情况是因为直接添加到队列不会触发创建线程/启动工作程序。这是一种边缘情况/竞争情况,可以通过具有非零的核心池大小来缓解
Krease

4

我知道这是一个老问题,但是存在一个类似的问题,即创建新任务的速度非常快,并且由于现有任务执行得不够快而导致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();
}

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密集型任务的大小大致相同,则可以使用此类,否则,请告诉我,我将发布基于大小的实现。

PS:您需要检查ThreadPoolExecutorjavadoc。这是Doug Lea很好的用户指南,说明如何轻松定制它。


1
我想知道当线程在beforeExecute()中持有锁并看到该锁maxTaskCount < currentTaskCount并开始等待unpaused条件时会发生什么。同时,另一个线程尝试获取afterExecute()中的锁以表示任务已完成。会不会陷入僵局?
塔希尔·阿赫塔尔

1
我还注意到,当队列已满时,此解决方案将不会阻塞提交任务的线程。所以RejectedExecutionException还是有可能的。
塔希尔·阿赫塔尔

1
ReentrantLock / Condition类的语义与synced&wait / notify提供的相似。当调用条件等待方法时,将释放锁定,因此不会出现死锁。
Petro Semeniuk

正确,此ExecutorService阻止提交时的任务,而不会阻止调用者线程。作业刚刚被提交,并且将在有足够的系统资源时被异步处理。
Petro Semeniuk

2

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

  • 指定正在进行的任务的最大数量
  • 指定等待任务执行许可的最大超时(如果超时过去并且没有获得许可,RejectedExecutionException则抛出a)
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);
    }
}

1

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

别理我...那是完全错误的。不会产生您需要的效果的ThreadPoolExecutor电话。Queue#offerput

您可以代替扩展ThreadPoolExecutor并提供execute(Runnable)该调用的实现。putoffer

恐怕这似乎不是一个完全令人满意的答案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.