Java ThreadPoolExecutor:动态更新核心池大小会间歇性地拒绝传入的任务


13

我遇到了一个问题,如果ThreadPoolExecutor在创建池之后尝试将核心池的大小调整为其他数字,那么RejectedExecutionException即使我提交queueSize + maxPoolSize的任务数从来没有超过,我也会间歇性地拒绝某些任务。

我试图解决的问题是扩展ThreadPoolExecutor,以基于坐在线程池队列中的挂起执行来调整其核心线程的大小。我需要这样做,因为默认情况下,只有在队列已满时,a ThreadPoolExecutor才会创建一个新的Thread

这是一个演示了该问题的小型独立的Pure Java 8程序。

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolResizeTest {

    public static void main(String[] args) throws Exception {
        // increase the number of iterations if unable to reproduce
        // for me 100 iterations have been enough
        int numberOfExecutions = 100;

        for (int i = 1; i <= numberOfExecutions; i++) {
            executeOnce();
        }
    }

    private static void executeOnce() throws Exception {
        int minThreads = 1;
        int maxThreads = 5;
        int queueCapacity = 10;

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                minThreads, maxThreads,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity),
                new ThreadPoolExecutor.AbortPolicy()
        );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
                0, 10, TimeUnit.MILLISECONDS);
        CompletableFuture<Void> taskBlocker = new CompletableFuture<>();

        try {
            int totalTasksToSubmit = queueCapacity + maxThreads;

            for (int i = 1; i <= totalTasksToSubmit; i++) {
                // following line sometimes throws a RejectedExecutionException
                pool.submit(() -> {
                    // block the thread and prevent it from completing the task
                    taskBlocker.join();
                });
                // Thread.sleep(10); //enabling even a small sleep makes the problem go away
            }
        } finally {
            taskBlocker.complete(null);
            scheduler.shutdown();
            pool.shutdown();
        }
    }

    /**
     * Resize the thread pool if the number of pending tasks are non-zero.
     */
    private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
        int pendingExecutions = pool.getQueue().size();
        int approximateRunningExecutions = pool.getActiveCount();

        /*
         * New core thread count should be the sum of pending and currently executing tasks
         * with an upper bound of maxThreads and a lower bound of minThreads.
         */
        int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));

        pool.setCorePoolSize(newThreadCount);
        pool.prestartAllCoreThreads();
    }
}

如果我提交的队列容量+ maxThreads不多,为什么池会抛出RejectedExecutionException。我从不更改最大线程数,因此按照ThreadPoolExecutor的定义,它应该将任务容纳在线程中或放入队列中。

当然,如果我从不调整池的大小,那么线程池将永远不会拒绝任何提交。这也很难调试,因为在提交中添加任何形式的延迟都会使问题消失。

关于如何修复RejectedExecutionException的任何指针?


为什么不ExecutorService通过包装一个现有的实现来提供自己的实现,该实现将重新提交由于调整大小而导致提交失败的任务?
丹牛

@daniu是一种解决方法。问题的重点是,如果我提交的队列容量+ maxThreads不多,为什么池应该抛出RejectedExecutionException。我从不更改最大线程数,因此按照ThreadPoolExecutor的定义,它应该将任务容纳在线程中或放入队列中。
Swaranga Sarma

好的,我似乎误解了您的问题。它是什么?您是否想知道行为为什么会发生或如何解决会给您带来麻烦?
丹牛

是的,将我的实现更改为执行程序服务是不可行的,因为很多代码都引用了ThreadPoolExecutor。因此,如果我仍然想拥有一个可调整大小的ThreadPoolExecutor,我需要知道如何解决它。进行此类操作的正确方法可能是扩展ThreadPoolExecutor并访问其某些受保护的变量,并在超类共享的锁上的同步块内更新池大小。
Swaranga Sarma

扩展ThreadPoolExecutor很可能不是一个好主意,在这种情况下您是否也不需要更改现有代码?最好提供一些示例,说明您的实际代码如何访问执行程序。如果它使用了许多特定于ThreadPoolExecutor(即不是ExecutorService)的方法,我会感到惊讶。
丹牛

Answers:


5

这是发生这种情况的一种情况:

在我的示例中,我使用minThreads = 0,maxThreads = 2和queueCapacity = 2来使其更短。第一条命令被提交,这是在方法execute中完成的:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    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);
}

对于此命令workQueue.offer(command),将执行addWorker(null,false)。工作线程首先在线程运行方法中将该命令从队列中移出,因此此时队列中仍然只有一个命令,

这次执行workQueue.offer(command)时,将提交第二条命令。现在队列已满

现在,ScheduledExecutorService执行resizeThreadPool方法,该方法使用maxThreads调用setCorePoolSize。这是setCorePoolSize方法:

 public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

此方法使用addWorker(null,true)添加一个工作程序。否,没有2个工作队列正在运行,最大数量且队列已满。

第三个命令被提交并失败,因为workQueue.offer(command)和addWorker(command,false)失败,从而导致异常:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@24c22fe rejected from java.util.concurrent.ThreadPoolExecutor@cd1e646[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at ThreadPoolResizeTest.executeOnce(ThreadPoolResizeTest.java:60)
at ThreadPoolResizeTest.runTest(ThreadPoolResizeTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:365)

我认为要解决此问题,您应该将队列的容量设置为要执行的最大命令数。


正确。通过将代码复制到自己的班级并添加记录器,我能够进行复制。基本上,当队列已满,并且我提交了新任务时,它将尝试创建新的Worker。同时,如果在那一刻,我的调整程序还将setCorePoolSize调用为2,这还将创建一个新的Worker。在这一点上,两个工作人员正在竞争要添加的对象,但两者都不能,因为这会违反最大池大小限制,因此新任务提交被拒绝。我认为这是一种竞争状况,因此我向OpenJDK提交了错误报告。让我们来看看。但是你回答了我的问题,所以你得到了赏金。谢谢。
Swaranga Sarma

2

不确定是否将其视为错误。这是在队列已满后创建其他工作线程时的行为,但是在Java文档中已经注意到,调用者必须处理被拒绝的任务。

Java文档

新线程的工厂。使用此工厂(通过方法addWorker)创建所有线程。必须为所有调用程序做好准备,以使addWorker失败,这可能反映出系统或用户的策略限制了线程数。即使未将其视为错误,创建线程的失败也可能导致新任务被拒绝或现有任务仍停留在队列中。

当调整核心池的大小,可以说增加,更多的工人被创建(addWorker在方法setCorePoolSize)和呼叫建立额外的工作(addWorker自法execute)时,被拒绝addWorker返回false(add Worker因为足够的额外工作人员最后的代码段)已由创建setCorePoolSize 但尚未运行以反映队列中的更新

相关零件

相比

public void setCorePoolSize(int corePoolSize) {
    ....
    int k = Math.min(delta, workQueue.size());
    while (k-- > 0 && addWorker(null, true)) {
        if (workQueue.isEmpty())
             break;
    }
}

public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    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);
}

private boolean addWorker(Runnable firstTask, boolean core) {
....
   if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
     return false;             
}

使用自定义重试拒绝执行处理程序(这应该适用于您的情况,因为您将上限设置为最大池大小)。请根据需要进行调整。

public static class RetryRejectionPolicy implements RejectedExecutionHandler {
    public RetryRejectionPolicy () {}

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
           while(true)
            if(e.getQueue().offer(r)) break;
        }
    }
}

ThreadPoolExecutor pool = new ThreadPoolExecutor(
      minThreads, maxThreads,
      0, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(queueCapacity),
      new ThreadPoolResizeTest.RetryRejectionPolicy()
 );

另请注意,您对shutdown的使用不正确,因为它不会等待提交的任务完成执行,而是使用with awaitTermination


根据JavaDoc,我认为shutdown等待已提交的任务:shutdown()启动有序的shutdown,在该命令中执行先前提交的任务,但不接受任何新任务。
Thomas Krieger

@ThomasKrieger-将执行已提交的任务,但不会等待它们完成-来自docs docs.oracle.com/javase/7/docs/api/java/util/concurrent / ...-此方法不等待先前提交的任务任务来完成执行。使用awaitTermination可以做到这一点。
Sagar Veeram
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.