如何在排队之前让ThreadPoolExecutor将线程增加到最大数量?


99

我一直对默认行为ThreadPoolExecutor支持ExecutorService很多人使用的线程池感到沮丧。引用Javadocs:

如果运行的线程数量超过corePoolSize但少于maximumPoolSize,则仅在队列已满时才创建新线程。

这意味着,如果使用以下代码定义线程池,则它将永远不会启动第二个线程,因为该线程LinkedBlockingQueue是无界的。

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

仅当您有一个受限制的队列并且该队列已满时,才会启动高于核心号的任何线程。我怀疑大量的初级Java多线程程序员都没有意识到这种行为ThreadPoolExecutor

现在,我有一个特定的用例,它不是最佳的。我正在寻找无需编写自己的TPE类的方法来解决该问题的方法。

我的要求是要对可能不可靠的第三方进行回调的Web服务。

  • 我不想与Web请求同步进行回调,因此我想使用线程池。
  • 我通常一分钟会收到几个,所以我不想拥有newFixedThreadPool(...)大量休眠的线程。
  • 我每隔一段时间就会收到大量此类流量,我想将线程数扩展到某个最大值(比如说50)。
  • 我需要尽最大的努力来完成所有回调,所以我希望将大于50的所有其他队列排队newCachedThreadPool()

我如何解决此限制,ThreadPoolExecutor即在需要启动更多线程之前队列必须被限制为已满的地方?如何排队任务之前启动更多线程?

编辑:

@Flavio很好地说明了使用ThreadPoolExecutor.allowCoreThreadTimeOut(true)来使核心线程超时和退出。我考虑过,但是我仍然想要核心线程功能。我不希望池中的线程数尽可能降低到核心大小以下。


1
假设您的示例最多创建10个线程,那么使用在固定大小的线程池上增长/缩小的东西是否真的节省了成本?
bstempi

好点@bstempi。这个数字有些随意。我将问题中的并发线程数增加到50。不确定我现在有多少个并发线程真正要工作,因为我有了这个解决方案。
2013年

1
真是的 如果我能在这里10个投票,我所处的位置完全相同。
尤金(Eugene)

Answers:


50

我如何解决此限制,ThreadPoolExecutor即在需要启动更多线程之前,队列必须被限制为已满。

我相信,我终于找到了针对此限制的某种优雅(也许有点怪异)的解决方案ThreadPoolExecutor。它包括扩展LinkedBlockingQueue使其返回falsequeue.offer(...)时候,已经有一些排队的任务。如果当前线程跟不上排队的任务,则TPE将添加其他线程。如果该池已处于最大线程数,则将RejectedExecutionHandler调用。然后是处理程序将其put(...)放入队列。

编写一个offer(...)可以返回false并且put()永不阻塞的队列肯定是很奇怪的,这就是hack的一部分。但这与TPE的队列使用情况很好,因此我认为这样做没有任何问题。

这是代码:

// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    private static final long serialVersionUID = -6903933921423432194L;
    @Override
    public boolean offer(Runnable e) {
        // Offer it to the queue if there is 0 items already queued, else
        // return false so the TPE will add another thread. If we return false
        // and max threads have been reached then the RejectedExecutionHandler
        // will be called which will do the put into the queue.
        if (size() == 0) {
            return super.offer(e);
        } else {
            return false;
        }
    }
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
        60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            // This does the actual put into the queue. Once the max threads
            //  have been reached, the tasks will then queue up.
            executor.getQueue().put(r);
            // we do this after the put() to stop race conditions
            if (executor.isShutdown()) {
                throw new RejectedExecutionException(
                    "Task " + r + " rejected from " + e);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});

通过这种机制,当我将任务提交到队列时,ThreadPoolExecutor将:

  1. 最初将线程数扩展到核心大小(此处为1)。
  2. 将其提供给队列。如果队列为空,则将其排队以由现有线程处理。
  3. 如果队列中已经有1个或多个元素,offer(...)则将返回false。
  4. 如果返回false,则按比例扩大池中的线程数量,直到达到最大数量(此处为50)。
  5. 如果达到最大值,则调用 RejectedExecutionHandler
  6. RejectedExecutionHandler随后会将任务到队列通过FIFO顺序第一个可用线程处理。

尽管在上面的示例代码中,队列是无界的,但是您也可以将其定义为有界队列。例如,如果您将容量添加到1000,LinkedBlockingQueue则它将:

  1. 将线程放大到最大
  2. 然后排队直到满载1000个任务
  3. 然后阻止呼叫者,直到队列可用空间为止。

此外,如果您需要在中使用offer(...)RejectedExecutionHandler则可以offer(E, long, TimeUnit)改用with Long.MAX_VALUE作为超时方法。

警告:

如果您希望在执行器关闭后将任务添加到执行器中,那么当执行器服务已关闭时,您可能想更明智地RejectedExecutionException放弃我们的习惯RejectedExecutionHandler。感谢@RaduToader指出这一点。

编辑:

对这个答案的另一个调整可能是询问TPE是否有空闲线程,而只有在空闲线程时才排队。您必须为此创建一个真正的类并ourQueue.setThreadPoolExecutor(tpe);在其上添加方法。

然后您的offer(...)方法可能类似于:

  1. 检查tpe.getPoolSize() == tpe.getMaximumPoolSize()在这种情况下是否只需致电super.offer(...)
  2. 否则,tpe.getPoolSize() > tpe.getActiveCount()则调用,super.offer(...)因为似乎有空闲线程。
  3. 否则返回false派生另一个线程。

也许这样:

int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}

请注意,TPE上的get方法很昂贵,因为它们访问volatile字段或(在的情况下getActiveCount())锁定TPE并遍历线程列表。同样,这里存在竞争条件,可能导致任务被不正确地排队,或者在存在空闲线程时分叉另一个线程。


我也遇到同样的问题,最终导致覆盖执行方法。但这确实是一个不错的解决方案。:)
巴蒂

尽管我不喜欢Queue为了达成此目的而违约
bstempi

3
您是否在这里感到奇怪,因为只有在新线程产生之后才将前几个任务排入队列?例如,如果您的一个核心线程正忙于执行一个长期运行的任务,而您调用execute(runnable),则runnable它只会添加到队列中。如果调用execute(secondRunnable),则将secondRunnable添加到队列中。但是现在,如果您调用execute(thirdRunnable),那么thirdRunnable它将在新线程中运行。在runnablesecondRunnable只运行一次thirdRunnable(或长时间运行的任务原件)完成。
罗伯特·图珀洛·施内克

1
是的,Robert是对的,在高度多线程的环境中,当有空闲线程要使用时,队列有时会增加。下面扩展TPE的解决方案效果更好。我认为罗伯特的建议应该标记为答案,即使上述骇客很有趣
想知道全部

1
“ RejectedExecutionHandler”帮助执行器在关闭时。现在您被迫使用shutdownNow(),因为shutdown()不会阻止添加新任务(由于需要)
Radu Toader

28

将核心大小和最大大小设置为相同的值,并使用允许从池中删除核心线程allowCoreThreadTimeOut(true)


+1是的,我想到了,但我仍然想拥有核心线程功能。我不希望在休眠期间线程池变为0个线程。我将编辑问题以指出这一点。但是很好。
灰色

谢谢!这是最简单的方法。
德米特里·奥夫钦尼科夫

28

关于这个问题,我已经有了另外两个答案,但是我怀疑这是最好的。

它基于当前接受的答案的技术,即:

  1. 覆盖队列的offer()方法以(有时)返回false,
  2. 这会导致ThreadPoolExecutor产生新线程或拒绝任务,并且
  3. 设置RejectedExecutionHandler实际排队上拒绝的任务。

问题是何时offer()应返回false。当队列中有几个任务时,当前接受的答案将返回false,但是正如我在评论中所指出的那样,这会导致不良后果。或者,如果您始终返回false,那么即使队列中有线程在等待,您也将继续产生新线程。

解决方案是使用Java 7 LinkedTransferQueue并进行offer()调用tryTransfer()。当有一个正在等待的使用者线程时,任务将被传递给该线程。否则,offer()将返回false ThreadPoolExecutor并将产生一个新线程。

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });

我必须同意,这对我来说看起来最干净。该解决方案的唯一缺点是LinkedTransferQueue是无限制的,因此如果没有额外的工作,您将不会遇到容量受限的任务队列。
Yeroc '17

当池增长到最大大小时会出现问题。假设池已扩展到最大大小,并且每个线程当前正在执行一个任务,则当可运行的线程提交时,此提议隐含的结果将返回false,并且ThreadPoolExecutor尝试添加线程,但是池已达到最大值,因此可运行的线程将被拒绝。根据您编写的rejectedExceHandler,它将再次被提供到队列中,从而导致这种猴子舞再次发生。
Sudheera

1
@Sudheera我相信你错了。 queue.offer(),因为它实际上是在调用LinkedTransferQueue.tryTransfer(),将返回false而不使任务排队。但是,RejectedExecutionHandler调用queue.put()不会失败并且不会使任务排​​队。
罗伯特·图珀洛-施内克

1
@ RobertTupelo-Schneck非常有用而且很好!
尤金(Eugene)

1
@ RobertTupelo-Schneck就像一个魅力!我不知道为什么Java中没有开箱即用的东西
Georgi Peev

7

注意:现在,更喜欢并推荐其他答案

这是一个让我感觉更直接的版本:每当执行新任务时,增加corePoolSize(直至maximumPoolSize的限制),然后每当执行以下任务时减小corePoolSize(至用户指定的“核心池大小”的限制)。任务完成。

换句话说,请跟踪正在运行或排队的任务的数量,并确保corePoolSize等于任务数量,只要它在用户指定的“核心池大小”和maximumPoolSize之间即可。

public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}

如所写,该类不支持在构造后更改用户指定的corePoolSize或maximumPoolSize,并且不支持直接或通过remove()或操作工作队列purge()


除了synchronized块,我喜欢它。您能否拨打队列以获取任务数。或者也许使用AtomicInteger
2013年

我想避免它们,但是问题是这样。如果execute()在单独的线程中有多个调用,则每个调用将(1)找出需要多少线程,(2)计算setCorePoolSize该数量,以及(3)调用super.execute()。如果步骤(1)和(2)不同步,我不确定如何防止在将核心池大小设置为更大的数字后将其设置为更低的数字的不幸排序。通过直接访问超类字段,可以使用“比较并设置”来代替,但是我看不出没有同步的子类中有一种干净的方法。
罗伯特·图珀洛-施内克

我认为只要该taskCount字段有效(即a AtomicInteger),那么针对该比赛条件的惩罚就相对较低。如果两个线程紧接彼此重新计算池大小,则它应该获得正确的值。如果第二个线程收缩了核心线程,那么它一定已经看到队列减少或其他原因。
灰色,

1
可悲的是,我认为这比那更糟。假设任务10和11调用execute()。每个人都会跟注atomicTaskCount.incrementAndGet(),他们将分别得到10和11。但是,如果不进行同步(通过获取任务计数和设置核心池大小),则可以得到(1)任务11将核心池大小设置为11,(2)任务10将核心池大小设置为10,(3)任务10调用super.execute(),(4)任务11调用super.execute()并入队。
罗伯特·图珀洛-施内克

2
我对该解决方案进行了认真的测试,这显然是最好的。在高度多线程的环境中,当有空闲线程时(由于具有TPE.execute的自由线程性质),有时仍会排队,但这种情况很少发生,这与竞标条件解决方案有更多机会发生,因此这种情况几乎发生在每次多线程运行中。
想知道全部

6

我们有一个子类,ThreadPoolExecutor该子类需要额外的creationThreshold覆盖execute

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

也许也有帮助,但是您的看上去当然更狡猾……


有趣。谢谢这个。我实际上不知道核心大小是可变的。
2013年

现在,我考虑了更多,从检查队列大小的角度来看,此解决方案比我的解决方案更好。我调整了答案,使该offer(...)方法仅有false条件地返回。谢谢!
2013年

4

推荐的答案仅解决了JDK线程池的一(1)个问题:

  1. JDK线程池倾向于排队。因此,它们不会产生新线程,而是将任务排队。仅当队列达到其限制时,线程池才会产生一个新线程。

  2. 当负载减轻时,线程不会退出。例如,如果我们有大量的作业到达池中,导致池达到最大,然后轻负载一次最多2个任务,则该池将使用所有线程来服务轻负载,从而防止线程退役。(仅需要2个线程…)

我对上述行为感到不满意,因此继续执行并建立了一个池来克服上述缺陷。

解决方案2)使用Lifo计划解决了该问题。Ben Maurer在2015年ACM应用大会上提出了这个想法: Systems @ Facebook scale

于是一个新的实现诞生了:

LifoThreadPoolExecutorSQP

到目前为止,该实现改善了ZEL的异步执行性能

该实现具有自旋功能,可以减少上下文切换的开销,从而在某些用例中产生出色的性能。

希望能帮助到你...

PS:JDK Fork Join Pool实现ExecutorService并作为“常规”线程池工作,实现是高性能的,它使用LIFO Thread调度,但是无法控制内部队列大小,退出超时...,最重要的是不能执行任务取消时中断


1
太糟糕了,此实现具有如此多的外部依赖关系。使它对我无用:-/
Martin L.

1
这是一个很好的观点(第二)。不幸的是,从外部依赖关系尚不清楚实现,但是如果需要,仍可以采用。
Alexey Vlasov

1

注意:我现在更喜欢并推荐其他答案

在更改队列以返回false的原始想法之后,我还有另一个建议。在这一步中,所有任务都可以进入队列,但是每当任务排队之后execute(),我们都会执行一个前哨无操作任务,该任务会被队列拒绝,从而产生一个新线程,该线程将立即执行无操作,然后执行队列中有东西。

由于工作线程可能正在轮询LinkedBlockingQueue新任务,因此即使有可用线程也可能使任务入队。为了避免即使在有可用线程的情况下也不会产生新线程,我们需要跟踪队列中有多少线程正在等待新任务,并且仅当队列中的任务多于等待线程时才产生新线程。

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

0

我能想到的最好的解决方案是扩展。

ThreadPoolExecutor提供了一些钩子方法: beforeExecuteafterExecute。在您的扩展中,您可以维护使用有界队列来馈送任务,并使用第二个无界队列来处理溢出。有人致电时submit,您可以尝试将请求放入有界队列中。如果遇到异常,只需将任务放在溢出队列中即可。然后,您可以利用afterExecute挂钩查看完成任务后溢出队列中是否存在任何内容。这样,执行者将首先处理其有界队列中的内容,并在时间允许的情况下自动从此无界队列中拉出。

似乎比您的解决方案还需要更多的工作,但至少它不涉及给队列意外的行为。我还认为,有一种更好的方法来检查队列和线程的状态,而不是依赖于异常抛出的异常。


我不喜欢这种解决方案。我很确定ThreadPoolExecutor不是为继承而设计的。
斯科特,

实际上,JavaDoc中有一个扩展权利的示例。他们指出大多数可能只是实现hook方法,但是它们告诉您在扩展时还需要注意什么。
bstempi

0

注意:对于JDK ThreadPoolExecutor,当您有一个受限制的队列时,只有在offer返回false时才创建新线程。您可能会使用CallerRunsPolicy获得一些有用的信息,它会创建一些BackPressure并直接在调用者线程中运行调用。

我需要从池创建的线程中执行任务,并有一个无数队列进行调度,而池中的线程数可能corePoolSizemaximumPoolSize之间增加减少

我最终从ThreadPoolExecutor进行了完整复制粘贴,并更改了execute方法,因为 不幸的是,扩展无法做到这一点(它调用了私有方法)。

我不想在新请求到达且所有线程都忙时立即产生新线程(因为我通常有短暂的任务)。我添加了一个阈值,但是可以根据您的需要随意更改(也许对于大多数IO来说,最好删除此阈值)

private final AtomicInteger activeWorkers = new AtomicInteger(0);
private volatile double threshold = 0.7d;

protected void beforeExecute(Thread t, Runnable r) {
    activeWorkers.incrementAndGet();
}
protected void afterExecute(Runnable r, Throwable t) {
    activeWorkers.decrementAndGet();
}
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable) null, false);
            }
            //>>change start
            else if (workerCountOf(recheck) < maximumPoolSize //
                && (activeWorkers.get() > workerCountOf(recheck) * threshold
                    || workQueue.size() > workerCountOf(recheck) * threshold)) {
                this.addWorker((Runnable) null, false);
            }
            //<<change end
        } else if (!this.addWorker(command, false)) {
            this.reject(command);
        }
    }
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.