不可能使缓存的线程池具有大小限制吗?


127

限制缓存线程池的创建数量似乎是不可能的。

这是在标准Java库中实现static Executors.newCachedThreadPool的方式:

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

因此,使用该模板继续创建固定大小的缓存线程池:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronusQueue<Runable>());

现在,如果您使用它并提交3个任务,一切都会好起来的。提交任何其他任务将导致拒绝执行异常。

试试这个:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runable>());

将导致所有线程按顺序执行。即,线程池将永远不会产生多个线程来处理您的任务。

这是ThreadPoolExecutor的execute方法中的错误吗?还是这是故意的?还是有其他方法?

编辑:我想要的东西完全类似于缓存的线程池(它会按需创建线程,然后在超时后将其杀死),但是它可以创建的线程数受到限制,并且一旦有线程就可以继续排队其他任务达到其线程限制。根据sjlee的答复,这是不可能的。查看ThreadPoolExecutor的execute()方法确实是不可能的。我将需要像SwingWorker一样子类化ThreadPoolExecutor并重写exe​​cute(),但是SwingWorker在execute()中所做的事情是一个完整的技巧。


1
你的问题是什么?您的第二个代码示例不是标题的答案吗?
rsp

4
我想要一个线程池,该线程池将根据任务数量的增加按需添加线程,但绝不能添加超过最大线程数量的线程。CachedThreadPool已经做到了,除了它将添加无限数量的线程并且不会以某些预定义的大小停止。我在示例中定义的大小为3。第二个示例添加了1个线程,但是由于新任务到达而其他任务尚未完成,因此不再添加两个线程。
Matt Crinklaw-Vogt

检查此问题,解决问题,debuggingisfun.blogspot.com
2012/05 /…

Answers:


235

ThreadPoolExecutor具有以下几种关键行为,这些问题可以解释您的问题。

提交任务后,

  1. 如果线程池尚未达到核心大小,它将创建新线程。
  2. 如果已达到核心大小,并且没有空闲线程,则它将任务排队。
  3. 如果已达到核心大小,则没有空闲线程,并且队列已满,它将创建新线程(直到达到最大大小)。
  4. 如果已达到最大大小,则没有空闲线程,并且队列已满,将启动拒绝策略。

在第一个示例中,请注意,SynchronousQueue本质上的大小为0。因此,当您达到最大大小(3)时,拒绝策略即开始(#4)。

在第二个示例中,选择的队列是LinkedBlockingQueue,它的大小不受限制。因此,您将陷入行为2。

您实际上不能对缓存类型或固定类型进行太多修改,因为它们的行为几乎完全确定了。

如果要有一个有限的动态线程池,则需要使用正的核心大小和最大大小以及有限大小的队列。例如,

new ThreadPoolExecutor(10, // core size
    50, // max size
    10*60, // idle timeout
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<Runnable>(20)); // queue with a size

附录:这是一个相当老的答案,并且当JDK的核心大小为0时,似乎改变了它的行为。自JDK 1.6起,如果核心大小为0并且该池没有任何线程,则ThreadPoolExecutor将添加一个执行该任务的线程。因此,核心大小0是上述规则的例外。感谢史蒂夫使这引起我的注意。


4
您必须写一些关于方法的文字allowCoreThreadTimeOut才能使这个答案完美。请参阅@ user1046052的答案
hsestupin 2013年

1
好答案!补充一点:其他拒绝策略也值得一提。请参阅@brianegge的答案
Jeff

1
行为2不应该说“如果已达到maxThread的大小,并且没有空闲线程,则它将任务排队。”
佐尔坦

1
您能否详细说明队列的大小意味着什么?这是否意味着只有20个任务可以在被拒绝之前排队?
佐尔坦

1
@Zoltán我前一阵子写了这个,所以从那以后可能会有一些行为发生变化(我没有紧跟最新的活动),但是假设这些行为没有变化,则#2如前所述是正确的,那就是也许这是最重要(也有些令人惊讶)的观点。一旦达到核心大小,TPE就倾向于使用排队而不是创建新线程。队列大小实际上就是传递给TPE的队列的大小。如果队列已满,但尚未达到最大大小,它将创建一个新线程(不拒绝任务)。参见#3。希望能有所帮助。
sjlee '16

60

除非我错过任何事情,否则原始问题的解决方案很简单。以下代码实现了原始海报所描述的所需行为。它将最多产生5个线程以在无限制队列上工作,空闲线程将在60秒后终止。

tp = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>());
tp.allowCoreThreadTimeOut(true);

1
你是对的。该方法是在jdk 1.6中添加的,因此对此知之甚少。而且,您不能拥有“最小”的核心池大小,这是不幸的。
jtahlborn 2011年

4
我唯一担心的是(来自JDK 8文档):“当在方法execute(Runnable)中提交新任务,并且运行的内核线程数少于corePoolSize时,即使其他工作线程创建了新线程来处理请求,线程空闲。”
veegee '16

相当确定这实际上不起作用。上次我看做上述事情的时候,即使您生成了5,实际上也只能在一个线程中运行您的工作。再过几年,但是当我沉迷于ThreadPoolExecutor的实现时,只有在您的队列已满时,它才分派给新线程。使用无限制的队列将导致这种情况永远不会发生。您可以通过提交工作并登录线程名称然后进行睡眠进行测试。每个runnable最终都会打印相同的名称/不能在其他任何线程上运行。
Matt Crinklaw-Vogt

2
这确实有效,Matt。您将核心大小设置为0,这就是为什么只有1个线程的原因。这里的技巧是将核心大小设置为最大大小。
T-Gergely

1
@vegee是正确的-这实际上不能很好地工作-ThreadPoolExecutor仅在corePoolSize以上时才重用线程。因此,当corePoolSize等于maxPoolSize时,您只会在池已满时从线程缓存中受益(因此,如果您打算使用此功能,但通常保持在最大池大小以下,则还可以将线程超时降低到一个较低的水平值;请注意,这里没有缓存-总是有新线程)
克里斯·里德尔

7

有同样的问题。由于没有其他答案可以解决所有问题,因此我添加了我的:

现在可以清楚地写在docs中:如果您使用不阻塞(LinkedBlockingQueue)的队列,则最大线程数设置无效,仅使用核心线程。

所以:

public class MyExecutor extends ThreadPoolExecutor {

    public MyExecutor() {
        super(4, 4, 5,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        allowCoreThreadTimeOut(true);
    }

    public void setThreads(int n){
        setMaximumPoolSize(Math.max(1, n));
        setCorePoolSize(Math.max(1, n));
    }

}

该执行人具有:

  1. 没有最大线程的概念,因为我们使用的是无界队列。这是一件好事,因为如果遵循通常的策略,此类队列可能导致执行者创建大量非核心的额外线程。

  2. 最大大小的队列Integer.MAX_VALUE。如果未完成的任务数超过,Submit()则会抛出。不知道我们会先耗尽内存还是会发生这种情况。RejectedExecutionExceptionInteger.MAX_VALUE

  3. 可能有4个核心线程。空闲的核心线程会在空闲5秒钟后自动退出,因此,是的,严格按需运行线程setThreads()

  4. 确保核心线程的最小数量不小于一个,否则submit()将拒绝每个任务。由于核心线程需要> =最大线程,因此该方法也setThreads()设置了最大线程,尽管最大线程设置对于无限制队列没有用。


我认为您还需要将'allowCoreThreadTimeOut'设置为'true',否则,一旦创建线程,便将其永久保留:gist.github.com/ericdcobb/46b817b384f5ca9d5f5d
eric

哎呀,我只是想念那个,对不起,那么您的回答是完美的!
eric

6

在您的第一个示例中,后续任务被拒绝,因为AbortPolicy默认是RejectedExecutionHandler。ThreadPoolExecutor包含以下策略,您可以通过以下setRejectedExecutionHandler方法进行更改:

CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy

听起来您想使用CallerRunsPolicy缓存线程池。


5

这里没有任何答案可以解决我的问题,这与使用Apache的HTTP客户端(3.x版本)创建有限的HTTP连接有关。由于花了我几个小时才找出一个好的设置,所以我将分享:

private ExecutorService executor = new ThreadPoolExecutor(5, 10, 60L,
  TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
  Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

这将创建一个ThreadPoolExecutor以五个开头的线程,并最多包含十个同时运行的线程CallerRunsPolicy用于执行。


该解决方案的问题在于,如果增加数目或生产者,则将增加运行后台线程的线程数。在许多情况下,这不是您想要的。
灰色,

3

根据ThreadPoolExecutor的Javadoc:

如果正在运行的线程数量大于corePoolSize但小于maximumPoolSize,则仅在队列已满时才创建新线程。通过将corePoolSize和maximumPoolSize设置为相同,可以创建固定大小的线程池。

(强调我的。)

抖动的答案是您想要的,尽管我的答案是您的其他问题。:)


2

还有另一种选择。除了使用新的SynchronousQueue之外,还可以使用任何其他队列,但是必须确保其大小为1,这样将强制executorservice创建新线程。


我认为您的意思是大小0(默认情况下),这样就不会有任务排队,真正迫使executorservice每次创建新线程。
Leonmax 2014年

2

看起来好像没有任何答案实际上可以回答问题-实际上我看不到这样做的方法-即使您从PooledExecutorService继承了子类,因为许多方法/属性都是私有的,例如,使addIfUnderMaximumPoolSize受保护,您可以请执行下列操作:

class MyThreadPoolService extends ThreadPoolService {
    public void execute(Runnable run) {
        if (poolSize() == 0) {
            if (addIfUnderMaximumPoolSize(run) != null)
                return;
        }
        super.execute(run);
    }
}

我得到的最接近的就是这个-但是即使那样也不是一个很好的解决方案

new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()) {
    public void execute(Runnable command) {
        if (getPoolSize() == 0 && getActiveCount() < getMaximumPoolSize()) {        
            super.setCorePoolSize(super.getCorePoolSize() + 1);
        }
        super.execute(command);
    }

    protected void afterExecute(Runnable r, Throwable t) {
         // nothing in the queue
         if (getQueue().isEmpty() && getPoolSize() > min) {
             setCorePoolSize(getCorePoolSize() - 1);
         }
    };
 };

PS没有测试以上


2

这是另一种解决方案。我认为此解决方案的行为符合您的期望(尽管不为此解决方案感到骄傲):

final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    public boolean offer(Runnable o) {
        if (size() > 1)
            return false;
        return super.offer(o);
    };

    public boolean add(Runnable o) {
        if (super.offer(o))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
};

RejectedExecutionHandler handler = new RejectedExecutionHandler() {         
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        queue.add(r);
    }
};

dbThreadExecutor =
        new ThreadPoolExecutor(min, max, 60L, TimeUnit.SECONDS, queue, handler);

2

这就是您想要的(至少我猜是这样)。有关解释,请检查乔纳森·费恩伯格(Jonathan Feinberg)答案

Executors.newFixedThreadPool(int n)

创建一个线程池,该线程池重用在共享的无界队列中操作的固定数量的线程。在任何时候,最多nThreads个线程都是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,则它们将在队列中等待,直到某个线程可用为止。如果在关闭之前执行过程中由于执行失败导致任何线程终止,则在执行后续任务时将使用新线程代替。池中的线程将一直存在,直到明确将其关闭。


4
当然,我可以使用一个固定的线程池,但这将使n个线程永远存在,或者直到我调用shutdown。我想要的东西完全类似于缓存的线程池(它会按需创建线程,然后在超时后将其杀死),但是它可以创建的线程数受到限制。
Matt Crinklaw-Vogt

0
  1. 您可以ThreadPoolExecutor按照@sjlee的建议使用

    您可以动态控制池的大小。有关更多详细信息,请查看此问题:

    动态线程池

    要么

  2. 您可以使用java 8引入的newWorkStealingPool API。

    public static ExecutorService newWorkStealingPool()

    使用所有可用处理器作为目标并行度级别来创建工作窃取线程池。

默认情况下,并行级别设置为服务器中的CPU核心数。如果您有4个核心CPU服务器,则线程池大小为4。此API返回ForkJoinPool类型,ExecutorService 并通过从ForkJoinPool中的繁忙线程中窃取任务来允许工作窃取空闲线程。


0

问题总结如下:

我想要的东西完全类似于缓存的线程池(它会按需创建线程,然后在超时后将其杀死),但是它可以创建的线程数受到限制,并且一旦遇到问题,就能够继续将其他任务排队线程限制。

在指出解决方案之前,我将解释为什么以下解决方案不起作用:

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());

当达到3的限制时,这不会将任何任务排队,因为根据定义,SynchronousQueue无法容纳任何元素。

new ThreadPoolExecutor(0, 3, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

这不会创建多个线程,因为如果队列已满,则ThreadPoolExecutor仅创建超出corePoolSize的线程。但是LinkedBlockingQueue永远不会满。

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<Runnable>());
executor.allowCoreThreadTimeOut(true);

在达到corePoolSize之前,这将不会重用线程,因为ThreadPoolExecutor会增加线程数,直到达到corePoolSize为止,即使现有线程处于空闲状态。如果您可以忍受这种劣势,那么这是解决问题的最简单方法。它也是“ Java Concurrency in Practice”(p172上的脚注)中描述的解决方案。

所描述问题的唯一完整解决方案似乎是涉及覆盖队列的offer方法并编写一个RejectedExecutionHandler解决此问题的方法,该问题的答案中对此进行了解释:如何使ThreadPoolExecutor在排队之前将线程数增加到最大?


0

这适用于Java8 +(及其他)。

     Executor executor = new ThreadPoolExecutor(3, 3, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>()){{allowCoreThreadTimeOut(true);}};

其中3是线程数的限制,而5是空闲线程的超时。

如果您想检查它是否可以自己运行,请执行以下代码:

public static void main(String[] args) throws InterruptedException {
    final int DESIRED_NUMBER_OF_THREADS=3; // limit of number of Threads for the task at a time
    final int DESIRED_THREAD_IDLE_DEATH_TIMEOUT=5; //any idle Thread ends if it remains idle for X seconds

    System.out.println( java.lang.Thread.activeCount() + " threads");
    Executor executor = new ThreadPoolExecutor(DESIRED_NUMBER_OF_THREADS, DESIRED_NUMBER_OF_THREADS, DESIRED_THREAD_IDLE_DEATH_TIMEOUT, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>()) {{allowCoreThreadTimeOut(true);}};

    System.out.println(java.lang.Thread.activeCount() + " threads");

    for (int i = 0; i < 5; i++) {
        final int fi = i;
        executor.execute(() -> waitsout("starting hard thread computation " + fi, "hard thread computation done " + fi,2000));
    }
    System.out.println("If this is UP, it works");

    while (true) {
        System.out.println(
                java.lang.Thread.activeCount() + " threads");
        Thread.sleep(700);
    }

}

static void waitsout(String pre, String post, int timeout) {
    try {
        System.out.println(pre);
        Thread.sleep(timeout);
        System.out.println(post);
    } catch (Exception e) {
    }
}

上面的代码对我来说的输出是

1 threads
1 threads
If this is UP, it works
starting hard thread computation 0
4 threads
starting hard thread computation 2
starting hard thread computation 1
4 threads
4 threads
hard thread computation done 2
hard thread computation done 0
hard thread computation done 1
starting hard thread computation 3
starting hard thread computation 4
4 threads
4 threads
4 threads
hard thread computation done 3
hard thread computation done 4
4 threads
4 threads
4 threads
4 threads
3 threads
3 threads
3 threads
1 threads
1 threads
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.