我想创建一个ThreadPoolExecutor
这样的方法,当它达到最大大小并且队列已满时,该submit()
方法在尝试添加新任务时会阻塞。我是否需要为此实现一个自定义RejectedExecutionHandler
,或者是否存在使用标准Java库执行此操作的现有方法?
我想创建一个ThreadPoolExecutor
这样的方法,当它达到最大大小并且队列已满时,该submit()
方法在尝试添加新任务时会阻塞。我是否需要为此实现一个自定义RejectedExecutionHandler
,或者是否存在使用标准Java库执行此操作的现有方法?
Answers:
我刚刚发现的可能解决方案之一:
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
因为这似乎是处理此类情况的标准方法。
throw e;
包含该词的原因。JCIP是正确的!
您可以使用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());
}
}
您应该使用CallerRunsPolicy
,在调用线程中执行被拒绝的任务。这样,在完成该任务之前,它无法将任何新任务提交给执行者,这时将有一些空闲池线程或该过程将重复。
从文档:
被拒绝的任务
当执行器已关闭时,并且在执行器对最大线程和工作队列容量使用有限范围且已饱和时,将拒绝在方法execute(java.lang.Runnable)中提交的新任务。无论哪种情况,execute方法都会调用其RejectedExecutionHandler的RejectedExecutionHandler.rejectedExecution(java.lang.Runnable,java.util.concurrent.ThreadPoolExecutor)方法。提供了四个预定义的处理程序策略:
- 在默认的ThreadPoolExecutor.AbortPolicy中,处理程序在拒绝时会抛出运行时RejectedExecutionException。
- 在ThreadPoolExecutor.CallerRunsPolicy中,调用执行自己的线程运行任务。这提供了一种简单的反馈控制机制,将降低新任务的提交速度。
- 在ThreadPoolExecutor.DiscardPolicy中,简单地删除了无法执行的任务。
- 在ThreadPoolExecutor.DiscardOldestPolicy中,如果未关闭执行程序,则将丢弃工作队列开头的任务,然后重试执行(该操作可能再次失败,导致重复执行此操作)。
另外,在调用ThreadPoolExecutor
构造函数时,请确保使用有限队列,例如ArrayBlockingQueue 。否则,任何事情都不会被拒绝。
编辑:根据您的评论,将ArrayBlockingQueue的大小设置为等于线程池的最大大小,并使用AbortPolicy。
编辑2:好的,我明白你的意思了。那怎么办:覆盖beforeExecute()
检查getActiveCount()
不超过的方法,如果不超过getMaximumPoolSize()
,请休眠并重试?
Hibernate有一个BlockPolicy
简单的方法,可以做您想做的事情:
/**
* 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 );
}
}
}
ThreadPoolExecutor
甚至可以直截了当地说:“方法getQueue()允许出于监视和调试的目的访问工作队列。强烈建议不要将此方法用于任何其他目的。” 这是被如此众所周知库提供,绝对是悲伤地看到。
在BoundedExecutor
上面引述的回答实践中的Java并发只有在您为执行程序使用无限制队列,或者信号量绑定不大于队列大小时才能正确运行。信号量是提交线程和池中线程之间的状态共享,即使队列大小<绑定<=(队列大小+池大小),也可以使执行程序饱和。
使用CallerRunsPolicy
仅在您的任务不会永远运行时才有效,在这种情况下,您的提交线程将rejectedExecution
永远存在,而如果您的任务需要很长时间才能运行,则是一个坏主意,因为提交线程无法提交任何新任务或如果它本身正在运行任务,则执行其他任何操作。
如果那是不可接受的,那么我建议在提交任务之前检查执行程序的有界队列的大小。如果队列已满,请等待一小段时间再尝试再次提交。吞吐量会受到影响,但是我建议这是一个比其他提议的解决方案更简单的解决方案,并且可以确保不会有任何任务被拒绝。
我知道这是一个hack,但是在我看来,这里提供的大多数hack都是干净的;-)
由于ThreadPoolExecutor使用阻塞队列“ offer”而不是“ put”,因此可以覆盖阻塞队列的“ offer”行为:
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));
我测试了它,它似乎起作用了。实施超时策略留给读者练习。
以下类包装了ThreadPoolExecutor并使用Semaphore进行阻止,然后工作队列已满:
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);
}
}
该包装器类基于Brian Goetz在《 Java Concurrency in Practice》一书中给出的解决方案。本书中的解决方案仅采用两个构造函数参数:Executor
用于信号量的an 和bound。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个许可)并且ALL池线程释放了它们的许可时,进入的任务就可以正确地获得2个许可。
现在,JCiP的解决方案使用起来有些麻烦,因为它没有强制执行所有这些约束(无限制的队列,或受那些数学限制的约束,等等)。我认为,这仅是一个很好的示例,它说明了如何基于已经可用的部分构建新的线程安全类,而不能作为成熟的可重用类。我认为后者不是作者的意图。
您可以像这样使用自定义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());
}
}
});
创建自己的阻塞队列,供执行程序使用,并使用您要查找的阻塞行为,同时始终返回可用的剩余容量(确保执行程序不会尝试创建比其核心池更多的线程,也不会触发拒绝处理程序)。
我相信这将使您获得所需的阻止行为。拒绝处理程序永远不会满足要求,因为这表明执行者无法执行任务。我可以想象的是,您会在处理程序中得到某种形式的“忙等待”。那不是您想要的,您想要阻止执行者的执行者队列...
ThreadPoolExecutor
使用offer
方法将任务添加到队列。如果我创建了一个自定义BlockingQueue
阻止的自定义offer
,则会破坏BlockingQueue
合同。
ThreadPoolExecutor
实现为什么要使用offer
而不是put
(阻止版本)的原因?另外,如果有一种方法可以让客户端代码告诉何时使用哪个客户端代码,那么尝试手动定制解决方案的许多人就会松了一口气
最近,我发现这个问题也有同样的问题。OP并未明确表示,但我们不想使用在RejectedExecutionHandler
提交者线程上执行任务的,因为如果此任务是长时间运行的任务,则会浪费工作线程。
阅读所有答案和评论,特别是使用信号灯的有缺陷的解决方案,或者使用afterExecute
I仔细查看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();
}
}
}
}
}
我相信,通过使用java.util.concurrent.Semaphore
和委派的行为,可以使用非常优雅的方法来解决此问题Executor.newFixedThreadPool
。新的执行程序服务仅在有线程执行时才执行新任务。阻塞由信号量管理,允许数量等于线程数量。任务完成后,它将返回许可。
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();
}
});
}
过去我有同样的需求:一种阻塞队列,每个队列由共享线程池支持,每个客户端的大小固定。我最终编写了自己的ThreadPoolExecutor:
UserThreadPoolExecutor(阻塞队列(每个客户端)+线程池(在所有客户端之间共享))
参见:https : //github.com/d4rxh4wx/UserThreadPoolExecutor
从共享的ThreadPoolExecutor中为每个UserThreadPoolExecutor提供最大线程数
每个UserThreadPoolExecutor可以:
我在弹性搜索客户端中找到了此拒绝策略。它在阻塞队列上阻塞了调用者线程。下面的代码
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;
}
}
最近,我需要实现类似的目标,但是需要在上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
这似乎是很好的解决方案。称为NotifyingBlockingThreadPoolExecutor。
编辑:此代码有问题,await()方法有问题。调用shutdown()+ awaitTermination()似乎可以正常工作。
我并不总是喜欢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());
我看到的唯一缺点是,调用线程可能被锁定的时间略长于严格必要的时间(最长250ms)。而且,由于此执行程序是有效地递归调用的,因此非常长的等待线程可用时间(小时)可能导致堆栈溢出。
尽管如此,我个人还是喜欢这种方法。它结构紧凑,易于理解并且运行良好。