• tomcat线程池-深度分析tomcat线程池设计与现实


    1.概述

           在正式进入Tomcat线程池之前,小伙伴们可以先回顾一下JDK中的线程池相关特性,对于JDK线程池的总结和源码的解析感兴趣的童鞋,也可参考博主的层层剖析线程池源码的这篇文章,文章主要讲述对线程池的生命周期核心参数常用线程池以及线程池源码做了详细的介绍。
            tomcat内部线程池的实现没有直接使用JUC下的ThreadPoolExecutor,而是选择继承JUC下的Executor体系类,然后重写execute()等方法,如下,为tomcat7.x下的线程池类图:
    在这里插入图片描述

    • tomcat的线程池不仅继承了JDK线程池Executor框架,同时为了将Executor纳入Lifecycle生命周期管理,也让它实现了Lifecycle接口
    • tomcat线程池对外暴露

    注意:不同版本下的tomcat,继承体系略有不同:

    1. 继承JUC原生ThreadPoolExecutor(7.x版本及以下),并覆写了一些方法,主要execute()和afterExecute()

    2. 继承JUC的AbstractExecutorService(7.x版本以上),从java.util.concurrent.ThreadPoolExecutor
      复制出一套代码,然后微调内部的execute()方法

    2. Tomcat 线程池源码

    2.1 线程池初始化

      protected void startInternal() throws LifecycleException {
            // 1.任务队列 
            taskqueue = new TaskQueue(maxQueueSize);
            // 2. 线程工厂
            TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
            // 3. 创建线程池,
            // ThreadPoolExecutor为 org.apache.tomcat.util.threads.ThreadPoolExecutor
            executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
            // 重建线程的时间间隔
            executor.setThreadRenewalDelay(threadRenewalDelay);
            // 是否需要预热核心线程
            if (prestartminSpareThreads) {
                executor.prestartAllCoreThreads();
            }
            // 线程池任务队列的 parent ,重点属性
            taskqueue.setParent(executor);
            // 设置组件的生命周期状态,lifcle 管理
            setState(LifecycleState.STARTING);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这个方法中,主要是初始化初始化org.apache.tomcat.util.threads.TaskQueue,同时构造org.apache.tomcat.util.threads.ThreadPoolExecutor的实例,org.apache.tomcat.util.threads.ThreadPoolExecutor构造方法与Java原生的线程池并没有任何不同,核心逻辑是直接调用父类[java.util.concurrent.ThreadPoolExecutor]的构造方法,只不过,在调用父类的构造方法之后,又调用了prestartAllCoreThreads()方法,用来预热核心线程。

    2.2 核心逻辑-任务执行

    在进入核心executor方法之前,先上图,演示tomcat线程池中的线程池是怎么处理提交过来的任务,小伙伴可仔细观察与原生JDK线程处理有何异同:
    在这里插入图片描述

            是不是很奇怪,与我们JDK线程池老八股文貌似有很大不同,这里是核心线程数如果满了,就判断最大线程数是否已经满了,如果没有满,则创建最大线程数;而JDK的原生线程池的的处理流是:

    • 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
    • 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
    • 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建 并启动一个线程来执行新提交的任务。
    • 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常

    带着这个问题,直接看核心executor方法:

        public void execute(Runnable command, long timeout, TimeUnit unit) {
            if ( executor != null ) {
                // 调用org.apache.tomcat.util.threads.ThreadPoolExecutor
                executor.execute(command,timeout,unit);
            } else {
                throw new IllegalStateException("StandardThreadExecutor not started.");
            }
        }
        // org.apache.tomcat.util.threads.ThreadPoolExecutor
        public void execute(Runnable command, long timeout, TimeUnit unit) {
            // 线程任务数加q
            submittedCount.incrementAndGet();
            try {
                // 父类:java.util.concurrent.ThreadPoolExecutor#execute()方法
                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;
                }
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

            可能看了一遍,也没发现有啥不同,Tomcat的线程池不就是调用原生的JDK线程池,然后在原生线程池执行拒绝策略的时候,尝试将任务再入一次任务队列,看起来貌似也没有毛病,不知道小伙伴有没有想起我们线程池的七大参数,在我们tomcat初始化的时候,传入的七大参数分别是什么:在Tomcat的线程池初始化的时候,其中尤其的阻塞队列,tomcat自己实现了的阻塞队列:org.apache.tomcat.util.threads.TaskQueue,其实问题就出现在该阻塞队列的实现上,下面,我们再看看org.apache.tomcat.util.threads.TaskQueue的源码

    2.2.1 org.apache.tomcat.util.threads.TaskQueue

    org.apache.tomcat.util.threads.TaskQueue 继承java.util.concurrent.LinkedBlockingQueuejava.util.concurrent.LinkedBlockingQueue 在初始化的时候,如果不指定默认长度,则其默认长度为Integer.MAX_VALUE

    2.2.1.2 TaskQueue#offer方法
        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);
            }
            // 提交任务的个数小于核心线程数
            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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

            为什么分析org.apache.tomcat.util.threads.TaskQueue#offer() 方法呢?因为在JDK原生线程池中,如果核心线程数已满,则会调用阻塞队列的offer() 方法向队列添加任务,如果添加失败,则创建核心最大线程来处理任务,试想,此处,如果最大线程数大于核心线程数,提交任务的时候,是不是直接就返回false,然后创建最大线程数的线程来处理任务,当最大线程数满的时候,执行拒绝策略-可参考层层剖析线程池源码第3章节源码解析,JDK原生线程池抛出异常,tomcat线程池然后通过org.apache.tomcat.util.threads.TaskQueue#force()方法添加任务:如下:为org.apache.tomcat.util.threads.TaskQueue#force()源码:

    public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
            if (this.parent != null && !this.parent.isShutdown()) {
                return super.offer(o, timeout, unit);
            } else {
                throw new RejectedExecutionException("Executor not running, can't force a command into the queue");
            }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

             org.apache.tomcat.util.threads.TaskQueue#force() 方法很简单,本质就是调用java.util.concurrent.LinkedBlockingQueueoffer() 方法,这样是不是很巧妙的修改了JDK原生线程池的执行流程

    2.3 tomcat线程池默认属性

    属性描述
    daemon(boolean)线程是否应该是守护程序线程,默认为 true
    namePrefix字符串)执行程序创建的每个线程的名称前缀。单个线程的线程名称将是namePrefix+threadNumber
    maxThreads(int)此池中活动线程的最大数量,默认为 200
    minSpareThreads(int)最小线程数(空闲和活动)始终保持活动状态,默认为 25
    maxIdleTime(int)空闲线程关闭之前的毫秒数,除非活动线程数小于或等于minSpareThreads。默认值为60000(1分钟)
    maxQueueSize(int)在我们拒绝之前可以排队等待执行的可运行任务的最大数量。默认值是Integer.MAX_VALUE

    3.总结

    tomcat 的线程池封装其实并不厚重,只是对 jdk 线程池做了简单优化,当线程池没有达到最大执行线程的时候,会优先开线程处理而不是直接将任务放入任务队列中,可能这样做,本身也是符合Tomcat的特性,因为Tomcat的处理的任务是IO请求,如果IO请求都是先放到任务队列等待,然后再处理,可能会极大降低tomcat的IO处理效率,这也是博主个人的思考,有不同看法的,欢迎留下评论,一起探讨。

  • 相关阅读:
    C++入门知识
    矩阵置零00
    Arduino下载与安装(Windows 10)
    使用springboot每日推送早安问候语到用户微信
    数据结构之数组
    LQ0046 凑算式【枚举】
    Matlab 机器人工具箱 动力学
    数据仓库: 2- 数据建模
    在微信小程序中如何引入iconfont
    SDL2 播放音频(MP4)
  • 原文地址:https://blog.csdn.net/the_one_and_only/article/details/127825542