【SpringBoot + Tomcat】【二】请求到达后端服务进程后的处理过程-连接的处理细节

1  前言

上节的后半部分,由于忙项目的事情去了,收尾的有点潦草,我们这节再继续。

上节我们的思路是先简单回顾了下,SpringBoot 启动和创建 Tomcat 的时机,然后我们还主要看了下 连接器 Connector 的创建已经启动过程。连接器本身很重要,因为它就像一个港口或者一个枢纽,连接着客户端和服务端,接收客户端的请求,然后进行信息打包交给服务端进行处理,服务端处理完,枢纽将数据写回客户端。

中间涉及到的一些关键类或者接口:Endpoint、Connector、Acceptor等,我们这节再加强一下认识。

2  剖析

我们从哪开始呢?我们思路的角度是什么呢?我这里是从线程的角度去理解的哈,就从 NioEndpoint 的 start() 方法回顾起,因为它的启动就标志着三类线程的启动,也标志着后端服务的大门正式开启了(可以处理请求了):

从这里我们可以看到三类线程池,大致看到一个请求的处理:

首先是 acceptor 线程,这个线程主要监听端口。当有请求过来的时候,完成 tcp 三次握手,将 accept 过来的 socket 注册OP_REGISTER 事件,并将该事件提交到 Poller 线程的事件队列 PollerEventQueue中 。

然后是 poller 线程,每一个 poller 实例都有一个 NIO selector 实例,同时也有一个事件队列SynchronizedQueue<PollerEvent>,轮询队列 SynchronizedQueue<PollerEvent>,该队列的事件由 acceptor 线程(监听到新连接时)或者 tomcat io 线程(处理完请求之后保持长连接,添加读事件)放入,然后根据不同的事件对原始socket注册相应的读写事件。每一个poller thread 来说,会调用 java NIO 对象 selector,发起系统调用,来监测原始 scoket 是否有读写事件发生,如果有则将原始 scoket 的封装对象交由 tomcat io 线程处理。

最后是我们的 Tomcat 线程池,接收 poller thread 传入的 scoket 封装对象后,依次调用 SocketProcessor/ConnectionHanlder(global instance)/Http11Processor/CoyoteAdapter,最后交由 servlet container 完成servlet API 的调用。

我们接下来,看几个小细节。

2.1  创建连接池 createExecutor

先来看下连接池的创建:

public void createExecutor() {
    internalExecutor = true;
    // 线程池的队列
    TaskQueue taskqueue = new TaskQueue();
    // 线程工厂 主要是标记线程名称的
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    // 线程池参数  minSpare默认是10 核心线程数 maxThreads 最大线程数默认是 200
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

注意这里的 ThreadPoolExecutor 它不是我们 juc包里的那个,这里是 Tomcat 的自己命名了一个,并且是继承了 juc 里边的那个:

// org.apache.tomcat.util.threads.ThreadPoolExecutor
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {...}

那么问题来了,区别是什么呢?我们看看:

(1)第一个区别是创建的时候,Tomcat 有个预创建的操作:

/**
 * Starts all core threads, causing them to idly wait for work. This
 * overrides the default policy of starting core threads only when
 * new tasks are executed.
 *
 * @return the number of threads started
 */
public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(null, true))
        ++n;
    return n;
}

可以看到它会预先把核心线程数的线程都先创建出来,这个大家应该理解吧,提前创建出来,这样有任务了的话,就可以快速执行了嘛。

(2)加了一个计数器的 submittedCount

会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。

提交任务的时候 ++:

public void execute(Runnable command, long timeout, TimeUnit unit) {
  submittedCount.incrementAndGet();
  ...
}

任务执行完的 --:

@Override
protected void afterExecute(Runnable r, Throwable t) {
    ...
    if (!(t instanceof StopPooledThreadException)) {
        submittedCount.decrementAndGet();
    }
}

(3)原生线程池拒绝策略后的重试

public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        super.execute(command);
    // 捕获原生拒绝策略抛出的异常
    } catch (RejectedExecutionException rx) {
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                // 再次放入队列,尽可能的执行
                if (!queue.force(command, timeout, unit)) {
                    // 若还是失败 就抛异常了
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                }
            } catch (InterruptedException x) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(x);
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }
    }
}

(4)第四个很重要的区别可能跟线程池本身没多大关系,而是跟队列有关系,就是这个 TaskQueue,它本身是继承 LinkedBlokingQueue.。

线程池提交任务的执行过程,不知道大家还记得不,我搜了一个图,大家看一下,就是当核心线程数满了后,会往队列里放 offer(不阻塞):

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();
    }
    // 核心线程数满了,走到这里 先执行 offer
    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);
}

而我们这里的队列 TaskQueue,我们看一下它的 offer 方法:

@Override
public boolean offer(Runnable o) {
    //we can't do any checks 这个不为空 因为放的就是线程池对象
    if (parent==null) return super.offer(o);
    //we are maxed out on threads, simply queue the object 当前线程数是否到达最大线程数 如果到了,那就只能放队列了
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
    //we have idle threads, just add it to the queue  提交的任务数小于线程数 说明有空闲的线程 可以放进队列 就能直接跑任务了
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
    //if we have less threads than maximum force creation of a new thread 当前线程数还没到最大线程数 直接返回 false
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

核心就在第三、四步,提交的任务数小于线程数直接放队列就能立马得到执行,到第四步,如果还没到最大线程数,它直接返回 false 再结合上边的线程池任务执行,他会走到最后的 addWorker 也就是创建新线程去执行了。所以可以看到它的执行方式跟传统的线程池的差异就是:tomcat 优先创建线程去执行任务了,而传统的会优先放队列哈。至于为什么要这么做?可能 tomcat 就是想快速的优先处理请求,将任务下发下去吧,有点类似 io 密集型的,线程数量多点,但是大多可能都在等待中,不会导致 cpu一直飙。

好了,这是线程池的一些细节。

2.2  请求计数器 initializeConnectionLatch

我们再看一个请求计数器,acceptor 线程用于接收客户端请求,那我总不能接上百万 上千万的吧,那我服务端岂不是要累死,是不是?

private int maxConnections = 8*1024;
protected LimitLatch initializeConnectionLatch() {
    if (maxConnections==-1) return null;
    if (connectionLimitLatch==null) {
        connectionLimitLatch = new LimitLatch(getMaxConnections());
    }
    return connectionLimitLatch;
}

默认的连接数最大为 8*1024,内部其实一个 Sync(共享锁方式实现) 、count 计数器、limit 最大限制

private final Sync sync;
private final AtomicLong count;
private volatile long limit;

再来看看执行:

所以说 tomcat 任务的线程池的并发量还要取决这个连接的配置哈。

3  小结

本节就看到这里哈,下节我们继续。

posted @ 2024-04-10 08:57  酷酷-  阅读(66)  评论(0编辑  收藏  举报