结论 :最近发现,如果对 java 执行程序进行了错误的调整,它将在保持空闲状态的同时拒绝请求!
在集成线上2个实例的时候使用的两个高容量服务时,我们使用专用 线程池 实现了请求处理处理,以限制我们的并发性并提高弹性。 我们将最小池大小设置为20个线程,将最大线程设置为300个线程。我们还希望避免客户端排队-我们宁愿适当地降级-因此我们将workQueue配置为大小1。
这个工作正常,但是在负载测试期间,我们注意到由于RejectedExecutionException而收到一些失败的请求:
java.util.concurrent.RejectedExecutionException: Task someInternalTaskName rejected from java.util.concurrent. ThreadPoolExecutor @2e95c205[Running, pool size = 300, active threads = 3, queued tasks = 0, completed tasks = 24301742]
! at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
! at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
! at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
这并非完全意外,它在ThreadPoolExecutor文档中被指出:
New tasks submitted in method execute(java. lang .Runnable) will be rejected… when the Executor uses finite bounds for both maximum threads and work queue capacity, and is saturated.
当执行程序对最大线程数和工作队列容量都使用有限范围并且已饱和时,在方法execute(java.lang.Runnable)中提交的新任务将被拒绝…
但是该例外明确表示没有排队的任务,并且查看我们的指标和线程转储,我们的大多数工作线程都位于TIMED_WAITING附近。
线程转储确认所有线程都已park,等待工作执行,而不是等待I/O或任何其他明显资源的阻塞。 因此,如果池中的线程都在等待,为什么它仍拒绝请求?
threadPool.execute(faceplam)
答案出现在ThreadPoolExecutor.execute(Runnable)方法中,该方法看起来像这样(为简化起见有一些简化和注释)。 请注意,RejectedExecutionException来自此处显示的最后一行。
public void execute(Runnable command) {
int poolState = ctl.get();
//If we have less than corePoolSize threads, always add a thread
if (workerCountOf(poolState) < corePoolSize) {
if (addWorker(command, true))
return;
poolState = ctl.get();
}
//Otherwise enqueue the item
if (workQueue.offer(command)) {
int recheck = ctl.get();
if (workerCountOf(recheck) == 0)
addWorker(null, false );
}
//If we're below max threadpool size, add a worker and exit
//If we're at max size, reject the command
else if (!addWorker(command, false)) {
reject(command);
}
}
你发现问题了吗?
————————————————————————————
该命令永远不会直接交给线程执行。 相反,该命令总是通过workQueue.offer(command)进入队列,然后工作线程从那里出队。 如果有许多调用execute()的生产者线程并且workQueue是有界的(特别是如果它很小),则有可能生产者在任何空闲的工作线程能够出队之前填充了工作队列。 在那种情况下,即使有很多空闲的工作线程,也会抛出RejectedExecutionExceptions。
该问题可以通过多种方式避免。
- 使workQueue足够大以缓冲工作项,以便使用者有时间在引入更多工作项之前将其出队(这是我们为此服务选择的选项)
- 而是使用SynchronousQueue-但请注意,这可能会导致Executor.execute(command)阻塞
- 使用无限制的队列-但这意味着项目可能会在队列中等待任意长的时间,并可能导致资源耗尽
如果要使用ThreadPoolExecutor构造函数创建池,则可以精确选择所需的策略。 从java.util.concurrent.Executors工厂方法创建的执行程序选择默认值,但有趣的是它们并不完全相同。
- newFixedThreadPool() 和 newSingleThreadExecutor()使用非绑定的队列
- newCachedThreadPool() 使用SynchronousQueue
- newWorkStealingPool() 使用完全不同的线程池实现ForkJoinPool,它具有一个内部工作队列实现,该实现的上限为6400万个项目。
通用的情况
我们的特殊问题在Java中,但这只是一个普遍问题的具体示例,如果你使用有界队列将生产者连接到使用者,则该队列必须足够大以允许使用者延迟。 否则,即使有足够的消费者能力,生产者也会在队列中竞争。 最后,值得一提的是,限制队列(以及所有结构)是一种最佳做法。