万字详解-JAVA线程池分析实战与利弊详解

前言

池化技术 我想作为一个JAVA开发者,这个词肯定并不陌生,其是什么意思呢?就是将对象放入池子,使用时从池中取,用完之后又交给池子,我们通过对池子的一些措施优化,实现对程序性能的调优。

那么JAVA中,池化技术的例子有哪些呢?

连接池:(比如数据库连接池,你想想你的项目中是否使用了 连接池druid 、hikari cp …)

对象池:(这个一般无很明显的例子,都是基于自身项目具体问题具体分析,针对获取代码昂贵的对象进行池化复用)

线程池:(控制线程,复用线程)

线程池无池化流程中的 从池中取交还给池的这两步逻辑,其本身是个 生产 消费模型,和一般的池化技术工作流程不太一致,这些文中会有讲解)


一、为什么需要线程池

java中的线程池或许是运用场景最多的并发框架,几乎所有涉及到异步 、并发任务执行的程序都可以使用线程池来优化解决,如果我们能够合理的使用线程池,其带来的好处是很多的

1、降低资源消耗

通常情况下在不使用线程池又要使用线程执行异步任务,我们需要new一个线程来运行,但线程的创建与销毁是需要开销的,并且在

在HotSpot VM虚拟机(sum jdk 、openJdk都是这个虚拟机)中,java创建的线程会被一 一映射到操作系统中,即程序每创建一个线程,我们所在的操作系统就会创建一个线程,当java线程终止时,操作系统线程也会被回收;使用线程池,我们可以复用已经创建的线程执行我们的任务降低频繁创建与销毁带来消耗。

下图展示的是我们的java程序线程与操作系统线程 一 一对应关系(我在程序创建10000个线程的线程池,且要求线程全部预热)

image-20230610163616091

2、提高响应速度

当任务到达时,或许不需要再创建线程(可能线程池已有空闲线程)就可以执行任务,且根据线程池的空闲数量,甚至可以同时执行多个任务

3、线程资源限制与管理

线程我们也可以视为一种资源,这个资源还是很宝贵的(毕竟都与操作系统直接关联了),如果我们没有限制手段,任其无限制的创建可能对程序本身而言是一场灾难,甚至可能影响到所在服务器相关的程序会其他应用软件;使用线程池我们可以对线程进行合理分配、监控、优化。

以上的操作仅限于对线程池知识足够了解,知道其运行原理或优缺点,设置了合理参数的情况;如果盲目的使用线程池,也许会适得其反,所以跟着我后边的内容,对线程池有个基础的认识与了解吧。

二、线程池类图与状态介绍

下边是一副线程池的类图介绍

本文直接从ThreadPoolExecutor入手,因为这才是线程池具体实现类,上边都是一些接口

ThreadPoolExecutor 在juc包下,即java.util.concurrent.ThreadPoolExecutor

首先,我们来介绍一下线程池的一些基础信息,比如状态、最大线程数 等等

image-20230610155014222

线程池状态和可容纳线程数

# 这个ctl用来表示线程池的状态以及线程数量 使用了Int
# 那么ctl的值 高三位就是用来标识了线程池的状态,余下的所有低位数就是用来标识了线程池线程数量,且我们可以看到,默认值是Running(运行),线程个数为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

# 最大线程个数,因不是所有平台Int都是32,这里我们假设Int就是32那么,线程池可创建最大线程数则为536870911
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

线程池的状态以及状态流转

线程池状态

# 线程池状态为运行:接受新任务并可以处理阻塞队列中的任务
private static final int RUNNING    = -1 << COUNT_BITS;

# 拒接新任务,只处理阻塞队列中余下的任务
private static final int SHUTDOWN   =  0 << COUNT_BITS;

# 拒接新任务,也不处理阻塞队列中余下的任务(队列中的任务被丢弃)
private static final int STOP       =  1 << COUNT_BITS;

# 包含阻塞队列中的所有任务都已执行完毕,当前活动线程数为0,即将调用Terminaled方法
private static final int TIDYING    =  2 << COUNT_BITS;

# 终止方法,terminaled方法调用后线程池的状态
private static final int TERMINATED =  3 << COUNT_BITS;

状态流转

RUNNING >>> SHUTDOWN: 显示调用了shutdown()或隐式的调用了finalize()方法中的shutdown()

RUNNING或SHUTDOWN >>> STOP:显示的调用了shutdownNow()

SHUTDOWN >>> TIDYING: 线程池活跃线程为0以及队列排队任务为空

TIDYING >>> TERMINATED: 当terminated()中钩子方法执行完成时

注意哈,上边的这些意思我也不是蒙的,这些在ThreadPoolExecutor类中,都是有详细的注释的,可以借助翻译工具进行阅读查看


我们将渐渐告别上边较为枯燥的介绍,一步步迈入实战线程池的道路,但首先,我们需要知道如何创建线程池,以及知道线程池中的参数有什么意义,这样,我们使用起来才能得心应手。


三、线程池参数介绍

一看到这个标题,可能有些大佬就会想起被面试官支配的一个问题(线程池用过吗?用哪些参数呢?有什么意义呢?放心,本文章一定不止干拉拉的说个字面意思,结合本文源码剖析与实战部分后,下次再遇到这个问题您一定会有所受益)

我们在第一次手动New 线程池的时候,一眼看去,可能会发出whats up的感叹,这特么居然有四个重载方法,至多有七个参数? 如下图

image-20230610161949060

image-20230610192513219

首先,我们先来说明下这几个个参数的意思

corePoolSize: 线程池核心线程个数

maximunPoolSize: 线程池最大线程数量

keepAlivetime: 存活时间,默认是针对于超过corePoolSize 的非核心线程部分的,当池中线程数大于核心线程,并且处于空闲状态,这会进行销毁(核心线程不会销毁);但是 如果线程池设置了allowCoreThreadTimeOut为true,无论是否是核心线程,只要空闲达到设置存活时间,都会进行销毁

unit:存活时间的时间单位 (毫秒 秒 分 时 天 等)

workQueue: 保存等待执行任务的一个阻塞队列,可以使用BlockingQueue的所有子类,比如常用的 基于数组的有界队列ArrayBlockingQueue 、基于链表的无界队列LinkedBlockingQueue ,同步队列SynchronousQueue

threadFactory: 线程工厂 可以对线程进行修饰 比如设置线程名 设置线程是否为守护队列等

handler: 拒接策略,当线程池所有线程均在执行任务且阻塞队列也满了的情况下仍有任务提交到线程池时触发,其定义了4种不同的策略,接下来我会进行详细讲解


四、线程池的执行流程

上边初步讲解了线程池的七个核心参数,接下来,我们结合七个核心参数,来看看线程池的执行流程,只有充分了解其执行流程,我们才可以将其运用在项目中,以及出现问题时才可以更快速的切入排查

(1)线程池执行示意图

下图是我画的一个线程池执行示意图,由于我画图功力实在菜了点,可能有小伙伴还是看不懂,放心,我还会结合流程图与文字进行叙述

image-20230610172849493

(2)线程池处理流程

首先,做一个说明,所谓任务,指的就是提交到线程池中自己编写业务逻辑

image-20230610174358198

(3)执行流程文字版

注意,每一次调用者线程进行任务提交都会经过以下执行流程步骤进行依次判断

1、首先判断线程池的核心线程数是否已满,(线程数小于设置的coreSize视作为未满),未满则创建一个新的线程,且将当前任务执行

2、核心线程数满了后,会尝试将任务丢入阻塞队列,如果队列未满,那么任务则是入列成功,自会有线程不断尝试从队列拉取任务后执行

3、如果队列已满,则判断线程池的线程数量是否已达到最大线程数(线程数小于设置的maximunPoolSize 视作为未满),未满则会创建一个新的线程,且将当前任务执行

4、如果最大线程也满了,则会根据设置的拒绝策略执行不同的拒绝措施(丢弃当前任务、抛出异常、丢弃队列中未执行的且最早提交的任务、使用调用者线程执行当前任务)

(4)源码分析

不如虎穴,焉得虎子,我们直接冲进源码去探个究竟,就算有些看不懂也无所谓,只要理清大致流程与脉络;我们基于以上执行流程,进行源码分析

(1)execute (执行任务)

下方代码是 java.util.concurrent.ThreadPoolExecutor类中 execute(Runnable command) 执行源码的主干分支代码逻辑,

我们来先给主干分支添加注释

image-20230610180808285

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 首先获取当前线程池状态和线程数的组合值
    int c = ctl.get();
    // 判断当前线程池线程数量是否小于设置的核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 尝试添加核心线程,将当前任务执行 (Worker是线程池内部类,对Thread做了包装,比如循环从阻塞队列拉取任务执行)
        if (addWorker(command, true)) {
            // 添加核心线程成功,且执行任务后直接返回
            return;
        }
        // 这里说明别的线程已经抢先添加了线程,导致核心线程满了,故此从新获取线程池状态和线程数组合值
        c = ctl.get();
    }
    // 到这一步,说明当前线程池核心线程已满
    // 判断线程池是否是运行状态以及尝试将当前任务加入阻塞队列
    if (isRunning(c) && workQueue.offer(command)) {
        // 二次检查 获取线程池状态和线程数组合值
        int recheck = ctl.get();
        // 如果线程池不是运行状则尝试删除当前任务与拒绝当前任务
        if (! isRunning(recheck) && remove(command)) {
            reject(command);
            // 否则如当前线程池现线程为空,则添加一个线程
        } else if (workerCountOf(recheck) == 0) {
            // 这里没有将当前任务传递给addWorker方法
            addWorker(null, false);
        }
    }
    // 到这里步 说明当前线程池线程数大于等于核心线程数,且队列已满
    // 尝试创建worker执行,这里第二个参数为fasle,方法内部根据false会判断是否达最大线程,创建失败则说明达到最大线程
    else if (!addWorker(command, false)) {
        // 线程数已达最大,则执行拒绝策略
        reject(command);
    }
}

(2)addWorker (添加工作线程)

下方使用了嵌套循环控制标签,我这里做个简单的介绍

retry:只是一个标签,这里其实代表重试的意思,但单词不一定是retry

使用 continue 标签名或break 标签名,有不同的效果

continue 标签名表示结束本次内部循环,从外部标签处重新开始执行内外嵌套循环,

break 标签名 这表示结束整个内外部循环

private boolean addWorker(Runnable firstTask, boolean core) {
    		// 标签
            retry:
            // 死循环
            for (;;) {
                // 获取线程池状态与线程数组合值
                int c = ctl.get();
                // 获取状态
                int rs = runStateOf(c);

              
                // 当线程池状态大于SHUTDOWN(stop trdying terminated)
                // 线程池状态为SHUTDOWN 且有了第一个任务
                // 线程池状态为 SHUTDOWN 且对列为空
                // 以上三种均返回false
                if (rs >= SHUTDOWN &&
                        ! (rs == SHUTDOWN &&
                                firstTask == null &&
                                ! workQueue.isEmpty())) {
                    return false;
                }
				// 内部嵌套死循环 尝试增加线程数
                for (;;) {
                    int wc = workerCountOf(c);
                    // 判断是否超过线程池可容纳最大线程数量,以及根据传入参数会灵活判断核心线程或最大线程
                    if (wc >= CAPACITY ||
                            wc >= (core ? corePoolSize : maximumPoolSize)) {
                        return false;
                    }
                    // cas尝试增加线程数
                    if (compareAndIncrementWorkerCount(c)) {
                        // 添加成功,这会结束循环,走到         boolean workerStarted = false;这一步
                        break retry;
                    }
                    // 没成功则再次获取线程池组合值
                    c = ctl.get(); 
                    //状态不同则从新从retry标签处开吃内外双循环
                    if (runStateOf(c) != rs) {
                        continue retry;
                    }
                    // 否则,由于workerCount更改,CAS失败;重试内部循环
                }
            }
			// 到这里说明cas尝试增加线程数成功
            boolean workerStarted = false;
            boolean workerAdded = false;
            Worker w = null;
            try {
                // 添加worker 内部创建了一个线程,
                w = new Worker(firstTask);
                final Thread t = w.thread;
                if (t != null) {
                    // 防止多个线程调用了提交方法,加独占锁,尝试同步workers
                    final ReentrantLock mainLock = this.mainLock;
                    mainLock.lock();
                    try {
                        // 再次检查状态,避免别的线程调用了shutdown()
                        int rs = runStateOf(ctl.get());
                        if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                            if (t.isAlive()) // precheck that t is startable
                            {
                                throw new IllegalThreadStateException();
                            }
                            // 添加当前worker至线程池已有worker列标
                            workers.add(w);
                            int s = workers.size();
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        // 解除独占锁
                        mainLock.unlock();
                    }
                    // 添加成功后则启动worker里的线程执行任务
                    if (workerAdded) {
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                // 任务启动失败则移除当前worker
                if (! workerStarted) {
                    addWorkerFailed(w);
                }
            }
            return workerStarted;
        }

图中是addWorker方法中调用的创建worker构造方法 w = new Worker(firstTask)

image-20230610190311376

(3)runWorker (worker又是如何执行任务的呢?)

image-20230610190445795

我们上方addWorker方法看到,其是用worker中的线程Thread启动了start()方法

根据调用链,其执行了Worker内部方法run ,因为 worker本身又实现了Runable方法

 private final class Worker extends AbstractQueuedSynchronizerimplements Runnable{
     private static final long serialVersionUID = 6138294804551838833L;
     final Thread thread;
     Runnable firstTask;
     volatile long completedTasks;

     Worker(Runnable firstTask) {
         setState(-1); // inhibit interrupts until runWorker
         this.firstTask = firstTask;
         this.thread = getThreadFactory().newThread(this);
     }
	
     // start启动后,就会执行到这里
     public void run() {
            runWorker(this);
      }
}

那么runWorker又做了什么事呢?

我们前面也说了,其会不断尝试从队列拉取任务执行,是否果真如此呢?

image-20230610191345829

    // 执行worker
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        // 立即需要执行任务(第一优先级任务,直接就run)
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        // 异常完成默认为true,这个在后续方法processWorkerExit继续解释
        boolean completedAbruptly = true;
        try {
            // 存在第一优先级任务 或者从阻塞队列中拉取到了任务
            while (task != null || (task = getTask()) != null) {
                // 获取锁
                w.lock();
                // 如果池正在停止,请确保线程被中断;如果没有,请确保线程未被中断。这需要在第二种情况下重新检查,以处理关闭清除中断时的无竞争
                if ((runStateAtLeast(ctl.get(), STOP) ||
                        (Thread.interrupted() &&
                                runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted()) {
                    wt.interrupt();
                }
                try {
                    // 执行任务前做什么事情,我们可以覆写ThreadPool里这个方法进行我们自己逻辑处理
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        // 执行任务,如果这里抛了异常,就会执行下方finally  processWorkerExit方法进行线程清理
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        // 执行后做什么事情,我们可以覆写ThreadPool里这个方法进行我们自己逻辑处理,比如日志记录存储?
                        afterExecute(task, thrown);
                    }
                } finally {
                    // 任务执行完成后,设置为null,那下一次循环就会从队列中获取了
                    task = null;
                    // 任务完成数+1
                    w.completedTasks++;
                    // 解锁
                    w.unlock();
                }
            }
            /**
             * 如果方法走到这里,说明当前线程至少已有了以下几个前置条件
             *  当前线程从队列没有获取到任务,getTask()返回了Null
             *  那,何时会返回null? 
             * 1、线程池状态大于 SHUTDOW且队列为空
             * 1、当前线程数以超过设置的核心线程数量,
             * 2 指定线程存活时间内没有任务提交到阻塞队列
             */
            // 异常完成设置为false
            completedAbruptly = false;
        } finally {
            // 线程清理方法,清理非核心线程
            processWorkerExit(w, completedAbruptly);
        }
    }

是不是发现,只要线程池线程数小于等于设置的核心线程,线程池就会一直工作?(take()队列,阻塞等待任务到来),这也是线程池在执行任务时,线程只要不是守护线程,程序不会退出的原因

(4)getTask(工作线程如何不断获取任务?)

不断死循环尝试从队列拉取任务,根据属性设置采用take阻塞当前线程直至拉取到任务或者根据设置的线程存活时间 poll拉取任务

        // worker尝试获取任务
        private Runnable getTask() {
            // 是否获取任务超时,默认false
            boolean timedOut = false; //
            //死循环
            for (;;) {
                int c = ctl.get();
                int rs = runStateOf(c);

                // 判断线程池状态是否大于 SHUTDOW且队列为空
                if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                    // 将工作线程数计数减一
                    decrementWorkerCount();
                    return null;
                }
                // 获取当前worker数量(线程池工作线程数)
                int wc = workerCountOf(c);

                // 是否启用worker(线程)存活时间校验策略, 那么设置了allowCoreThreadTimeOut=true,或者当前线程池线程数大于设置的核心数就会启用
                boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
                //---以下情况尝试将工作线程数计数减一
                // 情况1 如果当前线程池工作线程数大于线程池设置最大线程数
                // 情况2 启用了存活时间校验,且获取任务超时标记为true 且 (当前线程数大于1 或者工作阻塞队列为空)
                if ((wc > maximumPoolSize || (timed && timedOut))
                        && (wc > 1 || workQueue.isEmpty())) {
                    if (compareAndDecrementWorkerCount(c)) {
                        // 工作线程计算减一成功,这里获取的任务返回为null,那外层runWorker方法while循环从队列获取数据的逻辑就执行结束了
                        return null;
                    }
                    // 工作线程计数减一失败,跳过此次循环
                    continue;
                }

                try {
                    // 这里可以说是 处理超过核心线程数且设置了存活时间后如何处理的点睛之笔了

                    /**
                     * 启用了存活时间,那当前线程指定一个超时时间从队列poll拉取任务(超时时间就是存活时间),指定时间没拿到就会返回null,
                     * 没有启用存活时间(那说明当前线程数并未超过设置核心线程数量以及未启用allowCoreThreadTimeOut),此时调用take阻塞当前线程
                     * 直到从队列拉取到数据
                     */
                    Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
                    // 拉取到了任务就返回,外层就会执行该任务
                    if (r != null) {
                        return r;
                    }
                    // 启用allowCoreThreadTimeOut 或线程数超过了设置的核心线程数量,且尝试从队列拉取任务到了指定超时时间没有拉取到,将是否获取任务超时设置为true
                    timedOut = true;
                } catch (InterruptedException retry) {
                    timedOut = false;
                }
            }
        }

image-20230610191623901

(5)addWorkerFailed(任务执行失败了怎么办?)

回到上一步 runWorker方法中,有个方法会判断worker是否运行成功执行

image-20230614094650679

没有成功执行则会删除线程

image-20230614094752453

这就是为什么我们在使用线程池执行我们业务逻辑,业务逻辑报错了的话,线程池中线程编号一直增加的原因,为什么说是线程编号而不是线程一直不断增加,因为线程池中我们设置了最大线程,即使增加也不会超过阈值,但其会因异常会不断地创建线程销毁线程,也是达不到线程复用的目的

image-20230614104735916

(6)processWorkerExit (如何清理多余的线程?)

processWorkerExit 是负责清理非核心线程的方法,走到了这里,代表非核心线程在指定存活时间内,未成功获取到任务并执行

 /**
     * 清理工作线程 worker
     * @param w
     * @param completedAbruptly
     */
    private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 如果是异常完成,则需要我们这里将worker工作线程数量减一,非异常完成的话,在getTask()方法内部就已经减一了
        if (completedAbruptly) {
            decrementWorkerCount();
        }
        // 获取全局锁
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 获取到全局锁后,将当前worker完成的任务计数同步至线程池属性,这样外部就可以获取到线程池整体工作完成情况
            completedTaskCount += w.completedTasks;
            // 线程池移除掉当前工作线程worker,移除后表示着线程池线程数少了一个
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
        // 尝试终止一个线程
        tryTerminate();

        int c = ctl.get();
        // 线程池状态小于STOP (RUNNING ,SHUTDOWN)
        if (runStateLessThan(c, STOP)) {
            // 不是异常完成
            if (!completedAbruptly) {
                // 拿到线程池最小常驻线程数,如果设置了所有线程都超时那最小就是0,否则就是核心线程数
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                // 如果为0,且对垒为空 又从新赋值为1
                if (min == 0 && ! workQueue.isEmpty()) {
                    min = 1;
                }
                // 当前工作线程数如果大于等于线程池最小常驻线程数直接返回,不再处理
                if (workerCountOf(c) >= min) {
                    return; // replacement not needed
                }
            }
            // 添加新线程,且无立即执行任务(该线程只会不断从队列获取任务)
            addWorker(null, false);
        }
    }

(7)RejectedExecutionHandler (拒绝策略又是如何实现的呢?)

从上方我已初步讲过,线程池的拒绝策略有4中

image-20230610192202928

CallerRunsPolicy

使用调用者线程执行

首先是判断了线程池的状态,如果线程池没有shutdown则使用调用者线程本身执行,否则则丢弃任务(点进源码查看,其实如果线程池状态只要不是Running,就会丢弃)

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public CallerRunsPolicy() { }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}
AbortPolicy

抛出异常

直接构建一个异常信息,当前任务丢了

    public static class AbortPolicy implements RejectedExecutionHandler {
        public AbortPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
DiscardOldestPolicy

丢弃队列中最早提交的任务,执行当前任务

从队列Pool一个数据 (队列特性 先进先出 因此是最早提交的任务被出列)丢掉,再执行当前任务

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }
DiscardPolicy

丢弃当前请求任务

可以看到任务参数来到了rejectedExecution方法,但方法内部无任何实现逻辑就直接走完了流程,可不是当前任务被丢弃了嘛,因为啥也没干就仅仅走了个过场

    public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

在我们查看了线程池工作流程的部分核心方法后,不难发现,线程池的执行流程便是通了!!!

二且我们可以看出,ThreadPoolExecutor的实现是一个生产消费模型(与其他的池化技术不太一致,仅有复用特性满足),用户提交任务至池中,就是生产者生产任务;池中的workers就是消费者角色,不断的直接执行或从workQueue拉取任务进行消费。

五、线程池实战

恭喜恭喜,历经千山万水,经过漫长的理论知识,终于来到了这里!!!!

请问还记得线程池执行流程吗?

简单做个总结:任务提交 >先核心 >再队列 >队列满了再扩容线程 > 拒绝策略

可能有朋友纳闷甚至怀疑人生,为什么我们的WEB应用服务器 比如Tomcat也使用了线程池,但却不是这样的执行流程呢?哈哈这个问题,我们留在最后…

使用线程池,应注重参数的合理性,这一点有反面教材可以参考我的另一篇文章

JDK Executors类创建的四种常用线程池剖析

请问你知道核心线程和非核心线程的区别吗?

从设计上来讲,其都是worker,并无区别,核心线程与非核心线程并无单独标识区分,如果工作线程(worker)数,超过了设置的coreSize,那么线程池里可能就包含了核心与非核心线程,一旦线程数超过coreSize,且在指定的存活时间内未从队列中获取到任务,那么就会尝试将其销毁,最终留下的线程数小于等于coreSize为止,留下的就是所谓的核心线程,留下的线程因为使用了take()方法,尝试从队列获取任务,其会一直被阻塞直至任务的到来,因此,线程池即使任务执行完毕,只要存在核心线程,那么线程池就仍会存活(这里也解释了使用main方法构建线程池提交了任务后,为什么任务结束了程序没有退出的原因,因为其核心线程还在苦苦的等它的任务呢)

———— —————— —————— 实战开始—————— ——————

(1)定义自己的线程池

有必要的话,我们最好是线程池隔离,即不同业务之间如果要是用线程池的话,那么就各自定义,比如我司是做车联网的,我们在处理 设备 车辆 定位 驾驶员等各自使用了不同的线程池

根据IO密集型 、CPU密集型 、组合型不同任务定义不同的核心线程与最大线程

如果一段代码块涉及大量Io操作(远程服务调用 、较慢的数据库交互、 文件交互 等于磁盘 )这些因为较为耗时,且线程阻塞时不会竞争与占用CPU,故此可以适当多开一些线程来增加CPU分片调度选择到此类未执行任务的几率,提高此类任务并行处理效率

如果一段代码涉及大量计算 (比如创建对象 、加解密 、序列反序列化 、我们项目中音视频编解码)等,会占用大量CPU资源的这类任务,则不适合创建过多的线程,线程多了反而会因为CPU上下文切换导致效率降低的可能

根据任务类型选择阻塞队列

这个其实也要根据任务量级以及任务处理复杂程度来定,小心太多把程序内存撑爆

设置线程工厂

假设,我们只传了如下几个参数,IDEA编译器就给出了建议,让我们定义ThreadFactory来定义线程名字

image-20230610194656170

这个建议是非常有必要的,正式开发我想7个参数都是必须必须填充的,因为我们在出现问题的时候,可以根据线程名字快速定位是哪个线程池,也可快速定位到是哪里出了异常,否则,长得大差不差的线程名会将我们搞得一团乱码。

ex:

# 使用guava包快速创建
new ThreadFactoryBuilder().setNameFormat("demo-%d").build()
    
----
# 自定义实现
public class MyThreadFactory implements ThreadFactory {
    private final AtomicInteger atomicInteger = new AtomicInteger();
    private final String threadNamePrefix;

    public MyThreadFactory(String threadNamePrefix) {
        this.threadNamePrefix = threadNamePrefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        // 设置为守护线程
        thread.setDaemon(true);
        thread.setName(threadNamePrefix + "-" + atomicInteger.incrementAndGet());
        return thread;
    }
}   

设置拒绝策略

拒绝策略一般根据任务重要性来选择,如果任务重要且不能丢,要么队列设置大一点,但又要注意内存问题;要么就是使用CallRunsPolicy,但使用CallRunsPolicy 如果程序处理太慢太慢,可能会渐渐将所有调用者线程阻塞住导致无法处理WEB请求

如果任务不是那么重要,丢了就丢了

我们也可以覆写拒绝策略,使其打印日志或者做一个被拒绝的数据存储,然后编写一个定时任务,查询后进行再次提交至线程池进行消费

ex:

@Log4j2
public class MyRejectHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (!executor.isShutdown()) {
            //tod o存储数据库
        }
    }
}

于是乎,你的线程池可能是这样

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
                Runtime.getRuntime().availableProcessors() + 1,
                1,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<Runnable>(1024),
                new MyThreadFactory("my-demo"), new MyRejectHandler());

(2)使用线程池

(1)不需要返回值

比如我们发送短信

我这里仅是mock

image-20230610204609581

这里呢,并不是说使用线程池后程序真的只是耗时了31,31仅仅是调用者线程的耗时,其余的短信发送,交由线程池去慢慢处理就行

(2)需要返回值

demo1,使用的是线程池的execute方法,此方法没有返回值,如果我们程序需要返回值的话则可以使用submit方法,但我一般不用,我通常是结合CompletableFuture

假设我们有10个车辆信息需要去远程查询,且车辆信息查询代价非常昂贵(耗时1s),且提供者只是提供了单个ID请求接口,如果串行化去请求,仅远程调用就会需要耗时 1s*10 =10s,那接口的性能简直不像话

串行化

image-20230610205931077

线程池优化

我电脑的核心为16线程,所以core为16

image-20230610211631423

可以看到在获取到执行结果的情况下,使用线程池+CompletableFuture 接口性能是使用串行化情况下无法想象的,对于CompletableFuture 可以查看我的另一篇博客【JAVA8】CompletableFuture使用详解


六、使用线程池的弊端

万事万物没有百分百的完美,线程池也是如此;对于使用线程池的优势,在为什么需要线程池那里就已经讲了,那么使用线程池有什么弊端呢?

1、提升了使用成本,如果不清楚线程池原理的人,胡乱设置一通线程池参数可能会给程序以及服务器带来灾难

2、任务顺序性被打乱,按顺序提交的任务,也可能不会按顺序被执行,具体可以回顾下线程池的执行流程(阻塞队列),worker执行流程(第一优先级任务)

3、execute提交任务注意异常捕获,否则也是达不到线程复用的目的 这一点可以回顾addWorker那里


七、Tomcat线程池为什么可以不按照线程池执行顺序处理请求?

再次回顾线程池执行流程

任务提交 >先核心 >再队列 >队列满了再扩容线程 > 拒绝策略

这一方式,我称之为惰性线程池,(因为非核心线程是在太懒惰了,要队列满以及核心都不行了再创建,摸鱼摸太久了),如果我们队列设置的比较大的话,可能线程池永远无法扩容至设置的最大线程

那,tomcat是如何改写的呢?

我这里以springboot-web默认引入的tomcat包作为切入点

image-20230610213142334

在类中org.apache.catalina.core.StandardThreadExecutor 给出了答案

image-20230610213600580

image-20230610213547905

定义了自己的阻塞队列 TaskQueue,源码位置:org.apache.tomcat.util.threads.TaskQueue

将线程池作为参数设置进了阻塞队列中

ThreadPoolExecutor又做了什么呢?注意了这里的ThreadPoolExecutor并不是JDK的,而是tomcat自己改写的,但名字相同,源码位置在org.apache.tomcat.util.threads.ThreadPoolExecutor

Tomcat ThreadPoolExecutor 改写了什么呢?

改了execute执行逻辑

image-20230610214502999

TaskQueue 又与JDK提供的阻塞队列有什么不同呢?

image-20230610220416840

我们来详解下覆写的offer方法

    @Override
    public boolean offer(Runnable o) {
      	// 首先要注意,parent,是线程池本身,只是作为了参数传入到了TaskQueue
        // 线程池为空,则直接调用父类方法入列
        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;
        // 托底方式,直接尝试入列
        return super.offer(o);
    }

综上,Tomcat线程池为了应对可能出现的海量WEB并发请求,自定义线程池继承JDK线程池且改写了execute方法,失败后会尝试再次入列;自定义了阻塞队列继承JDK阻塞队列,将当前线程池作为一个参数又赋值到了队列中一个属性,并改写了offer方法,根据线程数与并发数灵活控制入列逻辑从而实现了一个激进线程池

那么tomcat的激进线程池的处理流程为:

任务提交 >先核心 >根据请求数动态选择是否扩容线程 >再队列 > 拒绝策略

————

暂时写到这里

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值