JavaSpringBoot

前言

解析SpringBoot内置Tomcat调优并发线程数的一些参数,并结合源码进行分析

参数

线程池核心线程数

server.tomcat.min-spare-threads:该参数为Tomcat处理业务的核心线程数大小,默认值为10

线程池最大线程数

server.tomcat.max-threads:该参数为Tomcat处理业务的最大线程数大小,默认值为200,当对并发量有一点值时可以调大该参数

请求最大连接数

server.tomcat.max-connections:该参数为请求的最大连接数,默认值为10000,注意这个参数并不是设置在线程池上的,而是在Tomcat的Acceptor类(专门处理连接的线程类)来控制的,结合源码可以看到
image.png

  1. protected void countUpOrAwaitConnection() throws InterruptedException {
  2. if (maxConnections==-1) return;
  3. LimitLatch latch = connectionLimitLatch;
  4. if (latch!=null) latch.countUpOrAwait();
  5. }

可以看到当最大连接数满了之后会进行等待

accept-count

server.tomcat.accept-count:这个参数实际上和Tomcat没有太大关系,默认值为100
先看下文档的定义

  1. /**
  2. * Allows the server developer to specify the acceptCount (backlog) that
  3. * should be used for server sockets. By default, this value
  4. * is 100.
  5. */
  6. private int acceptCount = 100;

这个参数是服务端创建ServerSocket时操作系统控制同时连接的最大数量的,服务端接收连接是通过accept()来的,accept()是非常快的,所以accept-count的不需要太大,正常保持默认值100即可了,acceptCount这个参数和线程池无关,会被映射为backlog参数,是socket的参数,在源码的使用是在NioEndpoint类的initServerSocket方法,在Tomcat中的名字是backlog在SpringBoot内置Tomcat中名字没有使用backlog而是使用acceptCount
**serverSock.socket().bind(addr,getAcceptCount())**

  1. protected void initServerSocket() throws Exception {
  2. if (!getUseInheritedChannel()) {
  3. serverSock = ServerSocketChannel.open();
  4. socketProperties.setProperties(serverSock.socket());
  5. InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
  6. // 核心代码
  7. serverSock.socket().bind(addr,getAcceptCount());
  8. } else {
  9. // Retrieve the channel provided by the OS
  10. Channel ic = System.inheritedChannel();
  11. if (ic instanceof ServerSocketChannel) {
  12. serverSock = (ServerSocketChannel) ic;
  13. }
  14. if (serverSock == null) {
  15. throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
  16. }
  17. }
  18. serverSock.configureBlocking(true); //mimic APR behavior
  19. }

Tomcat线程池处理机制

Tomcat最终使用线程池来处理业务逻辑,java默认的线程池的规则:
核心线程数满了则优先放入队列,当队列满了之后才会创建非核心线程来处理,那么Tomcat是这样做的吗?
首先如果Tomcat这样做,那么当达到核心线程后后续任务就需要等待了,这显然是不合理的,通过源码来看下Tomcat是如何处理的
AbstractEndpointcreateExecutor创建了处理业务数据的线程池

  1. public void createExecutor() {
  2. internalExecutor = true;
  3. TaskQueue taskqueue = new TaskQueue();
  4. TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
  5. executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
  6. taskqueue.setParent( (ThreadPoolExecutor) executor);
  7. }

主要是使用了TaskQueue队列,ThreadPoolExecutor并不是jdk的,而是Tomcat重写的。
从线程池的处理方法execute看起

  1. public void execute(Runnable command) {
  2. execute(command,0,TimeUnit.MILLISECONDS);
  3. }
  4. public void execute(Runnable command, long timeout, TimeUnit unit) {
  5. submittedCount.incrementAndGet();
  6. try {
  7. // 核心代码
  8. super.execute(command);
  9. } catch (RejectedExecutionException rx) {
  10. if (super.getQueue() instanceof TaskQueue) {
  11. final TaskQueue queue = (TaskQueue)super.getQueue();
  12. try {
  13. if (!queue.force(command, timeout, unit)) {
  14. submittedCount.decrementAndGet();
  15. throw new RejectedExecutionException("Queue capacity is full.");
  16. }
  17. } catch (InterruptedException x) {
  18. submittedCount.decrementAndGet();
  19. throw new RejectedExecutionException(x);
  20. }
  21. } else {
  22. submittedCount.decrementAndGet();
  23. throw rx;
  24. }
  25. }
  26. }

又调用会jdk的execute

  1. public void execute(Runnable command) {
  2. if (command == null)
  3. throw new NullPointerException();
  4. int c = ctl.get();
  5. // 1、工作线程数小于核心线程数则添加任务,核心线程会处理
  6. if (workerCountOf(c) < corePoolSize) {
  7. if (addWorker(command, true))
  8. return;
  9. c = ctl.get();
  10. }
  11. // 2、工作线程不小于核心线程数,则放到workQueue队列中
  12. if (isRunning(c) && workQueue.offer(command)) {
  13. int recheck = ctl.get();
  14. if (! isRunning(recheck) && remove(command))
  15. reject(command);
  16. else if (workerCountOf(recheck) == 0)
  17. addWorker(null, false);
  18. }
  19. // 3、否则添加任务,addWorker会进行创建线程
  20. else if (!addWorker(command, false))
  21. reject(command);
  22. }

从这里可以看到jdk线程池的机制,Tomcat使用了自己的TaskQueue队列,所以看代码2处当核心线程用完了会调用队列的offer方法
看TaskQueue的offer

  1. public boolean offer(Runnable o) {
  2. //we can't do any checks
  3. // parent就是指线程池,没有线程池则添加到队列
  4. if (parent==null) return super.offer(o);
  5. //we are maxed out on threads, simply queue the object
  6. // 线程数量已经达到了最大线程数,那么只能添加到队列
  7. if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
  8. //we have idle threads, just add it to the queue
  9. // 如果当前处理的任务数量小于当前线程池中线程的数量,那么任务放到线程池,即相当于马上会有空闲线程来处理
  10. if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
  11. //if we have less threads than maximum force creation of a new thread
  12. // TODO 核心代码,如果当前线程数量还没有达到线程池最大线程池的数量,那么就直接创建线程,这里返回false
  13. if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
  14. //if we reached here, we need to add it to the queue
  15. // 最后的策略,放到队列
  16. return super.offer(o);
  17. }

可以看到当执行offer时,不是直接放到队列的,当线程池总线程数量还没达到线程池最大线程数时会返回false,返回false时就会执行线程池execute的代码3处,执行addWorker(command, false),也就开始创建新的线程来处理当前任务了。

总结

Tomcat主要通过使用自己的TaskQueue队列来对线程池做出了不同的策略,也就是Tomcat当线程数大于核心数时就会直接创建新的线程来处理,而不是放到队列。