为什么使用的线程数比要求的多?

时间:2019-06-03 16:26:57

标签: java multithreading spring-boot threadpoolexecutor

我有一个SpringBoot应用程序,最多允许45个并发请求。 现在, 1个请求在旅途中使用threadPool A并行调用 16个外部服务。因此,请牢记一般情况和最坏情况,我对其进行了以下配置:

ThreadPoolTaskExecutor A = new ThreadPoolTaskExecutor();
A.setCorePoolSize(400);
A.setMaxPoolSize(1000);
A.setQueueCapacity(10);
A.setThreadNamePrefix("async-executor");
A.initialize();

我在这里的期望是,将最多使用 45 * 16 = 720个线程。但是在运行负载测试时,我发现线程一直保持打开状态(已在线程转储中检查),几分钟后它开始提供RejectedExecutionException。

RejectedExecutionException
Task ServiceX rejected from org.springframework.scheduling.concurrent.
ThreadPoolTaskExecutor$1@4221a19e[Running, pool
size = 1000, active threads = 2, queued tasks = 10, completed tasks = 625216]

大多数线程如转储中所示

"executor-A-57" #579 prio=5 os_prio=0 tid=0x000000000193f800 nid=0x2e95 waiting on condition [0x00007fa9e820c000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000582dadf90> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
    at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
    - None

我想知道我在这里想念的是什么?为什么我会被拒绝?

编辑: 我试图在一小段代码上复制类似的东西,就这样:

MainClass运行长循环。在每个循环内,它会调用service1 3次。现在,我有演示服务,其中仅包含相同的代码Thread.sleep(100)

MainClass.java

package com.flappy.everything.threadpooling;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class MainClass {

    private static ThreadPoolTaskExecutor getExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setMaxPoolSize(4);
        threadPoolTaskExecutor.setThreadNamePrefix("async-exec");
        threadPoolTaskExecutor.setCorePoolSize(4);
        threadPoolTaskExecutor.setQueueCapacity(2);
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolTaskExecutor outerExecutor = getExecutor();
        List<Service1> services = Arrays.asList(new Service1(), new Service1(), new Service1());
        for (int i = 0; i < 1000000; i++) {
            List<Future> futures = new ArrayList<>();
            for (Service1 service : services) {
                futures.add(outerExecutor.submit(() -> {
                    try {
                        service.set();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }));
            }
            for (Future future : futures) {
                future.get();
            }
        }
    }
}

Service1.java

package com.oyorooms.everything.threadpooling;

import org.springframework.scheduling.annotation.Async;

public class Service1 {
    public void set() throws InterruptedException {
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName());
    }
}

因此,理想情况下,我给的threadPool应该只打开3个线程,但在运行代码时仍然会出现拒绝。

2 个答案:

答案 0 :(得分:3)

这很有趣。

您列出的代码失败的原因是,将元素从工作队列转移到工作线程所需的时间比主线程将项目放入队列所需的时间慢。

流程如下:

if(there are active threads and is there availability on the queue){
    submit to the work queue for the worker threads to pick up // 1
} else {
   if(max pool size is not met){
      create a new thread with this task being its first task // 2
   } else { 
      reject // 3
   }
} 

您将看到代码打到// 3

首次提交任务时,线程数将小于最大池大小。提交的第一轮任务将到达// 2

第一次迭代后,活动线程数将是最大池大小,并且代码将尝试提交给// 1

让我们说主线程非常快地将3个项目放入队列,以使ThreadPool中的4个线程无法足够快地完成一个任务。如果发生这种情况,我们将传递第一个if语句(因为队列上没有可用性),然后转到else。由于已达到最大池大小,因此除了reject别无其他。

这可以通过检查ThreadPoolExecutor Javadocs来进一步解释。

  

如果无法将请求排队,则将创建一个新线程,除非该线程超过了maximumPoolSize,在这种情况下,该任务将被拒绝。

及以后

  

直接切换通常需要无限制的maximumPoolSizes,以避免拒绝新提交的任务。反过来,当平均而言,命令继续以比其可处理的速度更快到达时,就可以实现无限线程增长的可能性。

要解决您的问题,您有两种合理的选择:

  1. 使用SynchronousQueue。提供给SynchronousQueue的线程将无限期等待,直到另一个线程获取该项目为止(如果它知道另一个线程正在等待接收该项目)。如果放置不成功(即,另一个线程没有立即将其释放),则您定义的固定队列大小将导致主线程返回(无阻塞)。要使用Spring使用SynchronousQueue,请将队列容量设置为零。 setQueueCapacity(0)。 同样来自Javadocs

      

    工作队列的一个很好的默认选择是SynchronousQueue,它可以将任务移交给线程,而无需另外保留它们。

  2. 将队列大小设置为大于或等于您希望提交的并发任务数。队列的大小通常可能不会达到该大小,但是将来会保护您。

答案 1 :(得分:2)

我建议通过添加1条记录器行来进行测试,以输出任务执行器,然后对16个不同的调用和45个请求进行计数。可能发生了许多事情。

  1. 也许ThreadPoolTask​​Executor不是bean,而spring是在您的应用中配置了另一个。
  2. 也许应用程序的其他部分也在使用异步调用

  3. 永远循环的代码中可能存在一些错误

等...

但是,如果您没有单元测试,那么一个不错的起点就是简单地记录正在发生的事情并分析您的日志。