• 【Tomcat专题】如何正确使用线程池


    前言

    我们知道,池化概念,是一种非常常见的资源优化手段,池化主要就是针对会被重复使用的某些资源进行管理,避免每次创建以及销毁而带来较高的使用代价。

    一、Java中的线程池

    核心参数

    首先我们要先搞清楚线程池中一些关键参数的含义

    corePoolSize:核心线程数,即线程池始终保持着corePoolSize个线程数。
    
    maximumPoolSize:线程池中最多允许创建maximumPoolSize个线程。
    
    keepAliveTime:假设corePoolSize是5,maximumPoolSize是6,因此有1个线程是非核心线程,那么这个非核心线程就会在空闲了
    keepAliveTime时间后被销毁。
    
    workQueue:这是一个阻塞队列,用于存放当前线程来不及处理的任务。
    
    threadFactory:创建线程的工厂,为每个线程起一个有意思的名称,方便问题排查。
    
    handler:拒绝策略,定义如果阻塞队列被放满了以后,接下来的任务如何处理。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    线程池使用注意

    为了方便使用,Java中提供了一些已经封装好的线程池,但他们都或多或少存在一些问题,比如:FixedThreadPool,由于其存放任务的队列,几乎是无限大的,因此就有可能造成大量的请求堆积,最终导致OOM发生。

    关于这一点在阿里开发手册中,也有提及:
    在这里插入图片描述

    总体对比

    在这里插入图片描述

    二、Tomcat中的线程池

    毫无疑问,线程池中的线程数以及队列大小是最重要的两个关键参数, 那我们就来看看Tomcat是如何选择的。

    下面是Tomcat中创建一个线程池执行器对象的方式,我们主要针对参数进行一些了解

    // 定制队列
    taskqueue = new TaskQueue(maxQueueSize);
    // 定制线程工厂
    TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. getMinSpareThreads,获取核心线程数,默认值为25
    
    • 1
    /**
     * min number of threads
     */
    protected int minSpareThreads = 25;
    
    public int getMinSpareThreads() {
        return minSpareThreads;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2. getMaxThreads,获取最大线程数,默认值200
    
    • 1
    /**
     * max number of threads
     */
    protected int maxThreads = 200;
    
    public int getMaxThreads() {
        return maxThreads;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    3. maxIdleTime,对应的就是keepAliveTime,线程闲置时间
    
    • 1
    /**
     * idle time in milliseconds
     */
    protected int maxIdleTime = 60000;
    
    • 1
    • 2
    • 3
    • 4

    最后一个线程工厂,也就是TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());

    4. namePrefix
    
    • 1
    protected String namePrefix = "tomcat-exec-";
    
    • 1
    5. daemon
    
    • 1
    protected boolean daemon = true;
    
    • 1
    6. getThreadPriority,默认优先级:Thread.NORM_PRIORITY
    
    • 1
    
    protected int threadPriority = Thread.NORM_PRIORITY;
    
    public int getThreadPriority() {
        return threadPriority;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    7. 现在还有一个最要的参数,就是taskqueue,Tomcat并没有直接使用JDK中的队列,而是自己重新定制一个。
    我们先注意观察,taskqueue队列构建时,传入的参数taskqueue = new TaskQueue(maxQueueSize);
    
    • 1
    • 2
    protected int maxQueueSize = Integer.MAX_VALUE;
    
    • 1

    可以看到,这也是一个几乎无限大的队列数,前面我们说过,应当避免构建无限大的队列,主要原因有两点,第一,可能会造成内存溢出,第二,最大线程数的设置将变的没有意义,关于第一点虽然的确存在内存溢出的风险,但实际上在Tomcat服务中一般到不会,因为Tomcat中的线程池主要就是处理HTTP请求,没有哪个系统能让一个请求到Tomcat上的HTTP请求积压到内存溢出。

    常规执行流程

    因此我们只要来看看Tomcat是如何解决第二个问题的?再这之前,我们应该先清楚一个调用流程
    在这里插入图片描述

    只有在当前活跃线程数大于核心线程数,且任务队列已满,且当前活跃线程数小于最大线程数时,才会构建新的线程。

    Tomcat中的特殊处理

    基本流程清楚后,我们再额外看一下Tomcat中的一些特殊处理。

    Tomcat中的线程池有一个额外的属性submittedCount,你可以简单的理解为就是一个计数器,每当有一个任务执行在执行时submittedCount就会加1,当任务执行完成后submittedCount就会减1,具体记录这个数字有什么作用,继续往下看就知道了。

    private final AtomicInteger submittedCount = new AtomicInteger(0);
    
    • 1
    public void execute(Runnable command, long timeout, TimeUnit unit) {
    	// 
        submittedCount.incrementAndGet();
        try {
            executeInternal(command);
        } catch (RejectedExecutionException rx) {
            if (getQueue() instanceof TaskQueue) {
                // If the Executor is close to maximum pool size, concurrent
                // calls to execute() may result (due to Tomcat's use of
                // TaskQueue) in some tasks being rejected rather than queued.
                // If this happens, add them to the queue.
                final TaskQueue queue = (TaskQueue) 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

    现在,我们再来看看TaskQueue队列到底做了什么,首先TaskQueue继承了LinkedBlockingQueue,并且实际上也没有修改什么,只是offer方法做了一些额外的处理。

    public class TaskQueue extends LinkedBlockingQueue<Runnable> {}
    
    • 1

    前面我们看到的getSubmittedCount的作用在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);
        }
    
    	// 如果执行到这,说明当前线程数大于核心线程数,且小于最大线程数,因此正常情况下会根据队列任务数来判定是否可以继续创建新线程,但tomcat中并不是这样的,其逻辑就在下面的几行代码中。
    	
        //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
        // 走到这说明当前提交的线程数大于当前活跃的线程数。
        
        // 因此再校验下当前活跃线程数是否小于最大线程数,如果小于,此时就可以创建新的线程了。
        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
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    因此Tomcat的线程池策略是,虽然任务队列是无限大的,但依然能够保证有机会创建新的线程。
    
    • 1

    Tomcat中的执行流程

    在这里插入图片描述

    总结

    Tomcat结合了自身应用场景的特点,使用了无限大的队列来承载任务,做到了尽量不干预业务本身,保证了请求一定会交给业务服务去处理,但同时又为了解决无限大队列后,无法创建新的线程的问题,Tomcat又重写了Java提供的ThreadPoolExecutor类中的execute方法,以及定制了TaskQueue任务队列,并主要修改了offer方法,使得在拥有无限大的任务队列的同时,也能有机会创建新的线程。

  • 相关阅读:
    EGL函数翻译--eglInitialize
    1-k8s集群安装报错CGROUPS_CPU: missing
    Go语言必知必会100问题-20 切片操作实战
    袋鼠云数栈UI5.0体验升级背后的故事:可用性原则与交互升级
    《FPGA接口与协议》专栏的说明与导航
    华为机试:敏感字段加密
    牛客刷题系列之进阶版(搜索旋转排序数组,链表内指定区间反转)
    剖析WPF模板机制的内部实现
    2739. 总行驶距离
    【管理咨询宝藏88】556页!公司经营分析内部培训
  • 原文地址:https://blog.csdn.net/CSDN_WYL2016/article/details/127703078