Tomcat-线程模型及设计精髓

一、Tomcat架构模型

1.1 Tomcat架构模型

在这里插入图片描述
我们知道Tomcat既要接收客户端请求,同时需要处理业务逻辑,做相关的转发。
那么Server端无疑需要根据其能力,划分为2个部分。

  • 连接器
    连接器主要处理请求,并将请求根据路径做转发。默认情况下最大连接数为8192。连接器就需要实现上述功能。
    Tomcat同时支持3中连接配置:
    • BIO(Blocking IO)阻塞式IO(Tomcat8.5后弃用)
    • NIO(NioEndpoint )(使用Epoll模型,当连接、读写事件完成后,回调阻塞的线程,让内核任务继续完成)
    • AIO (Nio2Endpoint)(Nio2异步非阻塞式IO,win系统可以实现,但JVM并未在Linux系统实现异步能力,Nio及Nio2仍然使用Epoll实现)
  • 连接器的主从模型
    maxConnections:与tomcat建立的最大socket连接数,默认8192
    服务器需要大的连接数,根据Doug Lea的可扩展IO逻辑,现在的网络IO项目均采用主从线程模型,详细信息可查Nio线程模型详解
    在这里插入图片描述
  • Engine容器
    Engine容器,实现多层包装的容器管道,管道中可以自定义实现能力,就像Nio中的管道In/OutBound处理器,分部完成各层业务逻辑。图见1.2链路模型。

1.2 链路模型

在这里插入图片描述

二、Tomcat线程模型

2.1 同步/异步、阻塞/非阻塞模型

  • 何为同步、何为异步?
    在Java开发中,我们尝尝为了提高开发效率,使用Future<?> task或CompletableFuture链式变成的方式submit执行任务,此时是异步提交任务,最终执行的结果再通过get(TimeUnits)的方式获取到,此时便是异步。
    一般不使用线程池,同步等待任务结果的便是同步。
  • 何为阻塞式IO、何为非阻塞式IO?
    很多人爱讲现实中烧茶喝水的案例来解释,当烧水时我站边旁边等待则是阻塞式的,我不在旁边等待而是去看电视了则是非阻塞式的,很贴切很形象,但似乎并未完全解释其定义。结合IO模型中read()方法,我们来看:
    • 阻塞式IO: 当执行read()方法后(此时CPU尚在用户态),CPU调用系统函数,客户端发送的数据尚未从网卡拷贝至用户态内存空间,此时则需要静静地等待数据。
    • 非阻塞式IO:当CPU在用户态执行read()方法后,不再等待客户端发送数据和数据从内核态到用户态的传输。当数据到达用户空间时会主动通知线程干活。
      在这里插入图片描述

2.2 从硬件看Tomcat连接线程模型

此处用最常用的Nio同步非阻塞模型来看

  • 蓝色线表示Server端执行accept()及read()方法的过程
  • 紫色线表示当用户传输数据到网卡后,操作系统执行的过程。

数据来到网卡后,只有内核空间可以执行对硬件的操作(将数据读取到),并回调注册了回调方法的阻塞等待任务,此时task_struct任务再次被激活,并来到运行任务队列,当CPU执行到该任务时,将线程上下文信息恢复到寄存器。CPU从内核态切换到用户态,将内核空间的数据拷贝至用户态堆数据中。(当然由于mmap映射区的存在,一些场景下可以实现不拷贝数据,而知识获取内核空间的地址,从然实现获取、修改内容的能力,进而实现零拷贝)
在这里插入图片描述

三、Tomcat设计精髓

<Service name="Catalina">
<!--
namePrefix: 线程前缀
maxThreads: 最大线程数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断
minSpareThreads: 核心线程数,默认设置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化核心线程
maxQueueSize: 最大的等待队列数,超过则拒绝请求 ,默认 Integer.MAX_VALUE
maxIdleTime: 线程空闲时间,超过该时间,线程会被销毁,单位毫秒
className: 线程实现类,默认org.apache.catalina.core.StandardThreadExecutor
-->
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-%d"
              prestartminSpareThreads="true"
              maxThreads="500" minSpareThreads="8"  maxIdleTime="10000"/>
<!--
maxThreads:  内置线程池最大线程数,默认设置 200
minSpareThreads:  内置线程池核心线程数,默认设置 10
maxConnections:与tomcat建立的最大socket连接数,默认8192
acceptCount: 操作系统底层请求队列的最大长度 ,默认值为100
-->
   <Connector port="8080" protocol="HTTP/1.1" executor="tomcatThreadPool"
              connectionTimeout="20000" maxConnections="8192"  maxThreads="500"
              redirectPort="8443" URIEncoding="UTF-8"/>
   <Engine xxx/>
   ......
</Service>            

3.1 LimitLatch限流计数器

/**

  • Shared latch that allows the latch to be acquired a limited number of times
  • after which all subsequent requests to acquire the latch will be placed in a
  • FIFO queue until one of the shares is returned.
    */

上文说过Tomcat因为最大线程数的限制,必须针对request请求进行限流,那么限流的方式如何?

LimitLatch需要实现何种功能?
1、计数器
2、计数自增,当达到预设限制时,需要拒绝

我们对比Tomcat限流器LimitLatch和Doug Lea的CountDownLatch类结构比较。我们知道CountDownLatch的作用是闭锁AQS实现及各种锁详细
在这里插入图片描述

CountDownLatch,在计数count量减1,直至所有均完成时结束。而次数需要做限流,则需要count自增,直至count来到Limit。且看Tomcat的核心修改如下。

  • 尝试获取锁:AtomicLong自增,当请求数超过限制时返回-1,并修正原有count
        @Override
        protected int tryAcquireShared(int ignored) {
            long newCount = count.incrementAndGet();
            if (!released && newCount > limit) {
                // Limit exceeded
                count.decrementAndGet();
                return -1;
            } else {
                return 1;
            }
        }

3.2 Tomcat线程池无界队列实现

3.2.1 JDK线程池实现

JDK常规的线程池需要先把Task交给核心线程,当核心线程数未满时则创建核心线程。核心线程池满了则将Task交给Queue,如何阻塞队列也满了,此时才创建非核心线程。下面是此前整理的线程池详解中总结的线程池流程图。
在这里插入图片描述
看JDK线程池执行任务源码:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
     
        int c = ctl.get();
        //Step1 如果运行的核心线程数<定义核心线程数,则创建核心线程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //Step2 判断线程池是否仍在运行,尝试加入到队列
        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);
        }
        //Step3 Queue.offer添加任务队列失败时才会添加非核心线程执行command任务
        else if (!addWorker(command, false))
            reject(command);
    }
3.2.2 Tomcat无界队列线程池实现
  • 创建Tomcat线程池:
    public void createExecutor() {
        internalExecutor = true;
        //注意看队列队列使用的是Tomcat自定义实现的LinkedBlockingQueue
        TaskQueue taskqueue = new TaskQueue();
        TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
        executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
        taskqueue.setParent( (ThreadPoolExecutor) executor);
    }
  • execute方法虽然与JDK的实现略微有调整,但实际执行无异。
  • 核心线程和非核心线程的定义
    maxThreads: 最大线程数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务来判断
    minSpareThreads: 核心线程数,默认设置 25
    
  • 再看 workQueue.offer(command) [实际执行TaskQueue的offer()]
    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) return super.offer(o);
        //当前线程数已达到最大线程数,新的任务添加到任务队列
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);
        //当前有空闲线程,只需将其添加到任务队列
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);
        //当前非核心线程数未满,返回false,强制创建线程
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;
        //if we reached here, we need to add it to the queue
        return super.offer(o);
    }

是的,需要再次摩拜Doug Lea,大神的代码处处有惊喜!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值