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

时间:2013-10-22 21:02:43

标签: java multithreading blockingqueue threadpoolexecutor

我一直对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-request同步回调,所以我想使用一个线程池。
  • 我通常会在一分钟内得到一些这样的内容,因此我不希望newFixedThreadPool(...)拥有大量主要处于休眠状态的线程。
  • 每隔一段时间我都会遇到这种流量的爆发,我希望将线程数量扩大到某个最大值(假设为50)。
  • 我需要做一个最佳尝试做所有回调,所以我想排队50以上的其他任何回调。我不想通过使用一个来覆盖我的其余网络服务器newCachedThreadPool()

我如何在ThreadPoolExecutor中解决此限制,其中队列需要在启动更多线程之前需要绑定并完整?如何在排队任务之前启动更多线程

修改

@Flavio提出了使用ThreadPoolExecutor.allowCoreThreadTimeOut(true)使核心线程超时并退出的好处。我考虑过这一点,但我仍然想要核心线程功能。如果可能的话,我不希望池中的线程数降到核心大小以下。

10 个答案:

答案 0 :(得分:40)

  

如何在ThreadPoolExecutor中解决此限制,其中队列需要在更多线程启动之前需要绑定并填满。

我相信我终于通过ThreadPoolExecutor找到了一个有点优雅(可能有点hacky)的限制解决方案。它涉及扩展LinkedBlockingQueue,以便在已经排队的某些任务时让false返回queue.offer(...)。如果当前线程没有跟上排队任务,TPE将添加其他线程。如果池已经处于最大线程,则将调用RejectedExecutionHandler。然后是处理程序将put(...)放入队列。

编写一个队列,offer(...)可以返回falseput()永远不会阻塞,这就是黑客部分,这当然很奇怪。但这适用于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);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
        }
    }
});

使用此机制,当我将任务提交到队列时,ThreadPoolExecutor将:

  1. 最初将线程数量扩展到核心大小(此处为1)。
  2. 将其提供给队列。如果队列为空,它将排队等待现有线程处理。
  3. 如果队列中已有1个或更多元素,则offer(...)将返回false。
  4. 如果返回false,则向上扩展池中的线程数,直到它们达到最大数量(此处为50)。
  5. 如果达到最大值,则会调用RejectedExecutionHandler
  6. RejectedExecutionHandler然后将任务放入队列中,由FIFO顺序中的第一个可用线程处理。
  7. 虽然在上面的示例代码中,队列是无界的,但您也可以将其定义为有界队列。例如,如果您向LinkedBlockingQueue添加1000的容量,那么它将:

    1. 将线程缩放到最大值
    2. 然后排队直到它已满1000个任务
    3. 然后阻止调用者,直到空间可用于队列。
    4. 此外,如果你真的需要在offer(...)中使用 RejectedExecutionHandler然后您可以使用offer(E, long, TimeUnit)方法而不是Long.MAX_VALUE作为超时。

      修改

      我根据@Ralf的反馈调整了offer(...)方法覆盖。如果它们没有跟上负载,这只会扩大池中的线程数。

      修改

      对此答案的另一个调整可能是实际询问TPE是否存在空闲线程,并且仅在存在项目时将其排队。您必须为此创建一个真正的类,并在其上添加ourQueue.setThreadPoolExecutor(tpe);方法。

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

      1. 检查tpe.getPoolSize() == tpe.getMaximumPoolSize()是否只调用super.offer(...)
      2. 否则tpe.getPoolSize() > tpe.getActiveCount()然后调用super.offer(...),因为似乎有空闲线程。
      3. 否则返回false以分叉另一个线程。
      4. 也许这个:

        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并遍历线程列表。此外,这里存在竞争条件,可能导致任务被错误地排队,或者在有空闲线程时分叉另一个线程。

答案 1 :(得分:23)

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

答案 2 :(得分:20)

我已经在这个问题上得到了另外两个答案,但我怀疑这个答案是最好的。

它基于the currently accepted answer的技术,即:

  1. 覆盖队列的offer()方法(有时)返回false,
  2. 导致ThreadPoolExecutor生成新线程或拒绝任务,
  3. RejectedExecutionHandler设置为实际将拒绝任务排入队列。
  4. 问题是offer()应该返回false。当队列上有几个任务时,当前接受的答案返回false,但正如我在评论中指出的那样,这会导致不良影响。或者,如果总是返回false,即使线程在队列中等待,您也会继续生成新线程。

    解决方案是使用Java 7 LinkedTransferQueueoffer()调用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();
                }
            }
        });
    

答案 3 :(得分:7)

注意:我现在更喜欢并推荐my other answer

这里的版本对我来说更直接:每当执行新任务时增加corePoolSize(最大为maximumPoolSize的限制),然后减少corePoolSize(降低到指定用户的限制&# 34;核心池大小&#34;)每当任务完成时。

换句话说,跟踪正在运行或排队的任务的数量,并确保corePoolSize等于任务数量,只要它在用户指定的核心池大小和#34之间。 ;和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()来操作工作队列。 / p>

答案 4 :(得分: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);
            }
        }
    }
}
也许这也有帮助,但你的当然看起来更具艺术性......

答案 5 :(得分:3)

建议的答案只能解决JDK线程池问题的一(1)个问题:

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

  2. 当负载变亮时,线程退役不会发生。例如,如果我们有一系列作业到达池中导致池达到最大值,然后一次轻载最多2个任务,则池将使用所有线程来为轻载提供服务以防止线程退出。 (只需要2个线程......)

  3. 对上述行为不满意,我继续实施了一个池来克服上述缺陷。

    要解决2)使用Lifo调度可以解决问题。 Ben Maurer在ACM applicative 2015会议上提出了这个想法: Systems @ Facebook scale

    所以一个新的实现诞生了:

    LifoThreadPoolExecutorSQP

    到目前为止,此实现改进了ZEL的异步执行性能。

    该实现具有旋转功能,可以减少上下文切换开销,从而为某些用例提供卓越的性能。

    希望它有所帮助...

    PS:JDK Fork加入池实现ExecutorService并作为&#34; normal&#34;线程池,实现是高性能的,它使用LIFO线程调度,但是无法控制内部队列大小,退出超时......,最重要的是在取消它们时不能中断任务

答案 6 :(得分:1)

注意:我现在更喜欢并推荐my other answer

我有另一个建议,遵循将队列更改为false的最初想法。在这一个任务中,所有任务都可以进入队列,但只要任务在execute()之后排队,我们就会使用队列拒绝的Sentinel no-op任务来跟踪它,导致一个新的线程产生,这将执行no -op紧接着队列中的东西。

因为工作线程可能正在轮询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();            
    }
});

答案 7 :(得分:0)

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

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

似乎比你的解决方案更多的工作,但至少它不涉及给队列意外的行为。我还想象有一种更好的方法来检查队列和线程的状态,而不是依赖于异常,这些异常很慢。

答案 8 :(得分:0)

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

我需要从池创建的线程中执行任务,并有一个无数队列来进行调度,而池中的线程数可能会增长缩小 corePoolSize maximumPoolSize 这样...

我最终从 ThreadPoolExecutor 做了完整复制粘贴,并更改有点执行方法,因为 不幸的是这不能通过扩展来完成(它调用私有方法)。

我不想在新请求到达并且所有线程都忙时立即产生新线程(因为我通常有短暂的任务)。我添加了一个阈值,但是可以根据您的需要随意更改(也许对于大多数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);
        }
    }

答案 9 :(得分:0)

下面是一个使用两个线程池的解决方案,它们的核心和最大池大小相同。当第一个池忙时使用第二个池。

import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyExecutor {
    ThreadPoolExecutor tex1, tex2;
    public MyExecutor() {
        tex1 = new ThreadPoolExecutor(15, 15, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        tex1.allowCoreThreadTimeOut(true);
        tex2 = new ThreadPoolExecutor(45, 45, 100, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
        tex2.allowCoreThreadTimeOut(true);
    }

    public Future<?> submit(Runnable task) {
        ThreadPoolExecutor ex = tex1;
        int excessTasks1 = tex1.getQueue().size() + tex1.getActiveCount() - tex1.getCorePoolSize();
        if (excessTasks1 >= 0) {
            int excessTasks2 = tex2.getQueue().size() + tex2.getActiveCount() - tex2.getCorePoolSize();;
            if (excessTasks2 <= 0 || excessTasks2 / (double) tex2.getCorePoolSize() < excessTasks1 / (double) tex1.getCorePoolSize()) {
                ex = tex2;
            }
        }
        return ex.submit(task);
    }
}