从测试代码揣摩spring线程池工作过程

目录

一、写在前面

二、简述线程池工作过程

2.1 组成

2.2 工作过程--快速任务

2.3 工作过程--慢任务

2.4 非核心线程复用

2.5 过期时间

2.6 拒绝策略

三、最后的结论

以上所有的东西,想表达的都是下图

四、所有的测试代码


一、写在前面

1. 线程池常说的7个参数是哪些我就不说了。资料很多

2. spring的线程池使用,阿里手册要求自定义。

但是我无意间看过一篇资料。https://blog.csdn.net/rubeson/article/details/104097134

首先,阿里的使用场景和我们公司,或者说大部分公司是不一样的。业务量没有那么高。

而直接使用定长或者单线程线程池的时候,阻塞队列(我叫它任务队列)大小默认是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

但是无论是jdk内部定义好的,还是我们自定义,到底层都是使用的 ThreadPoolExecutor来新建

二、简述线程池工作过程

2.1 组成

任务队列:BlockingQueue<Runnable> workQueue

线程集合:HashSet<Worker> workers

PS: 

a. Worker可以理解为是把线程Thread包装了一下,内部有个Thread装载线程,有个Runnable装载要执行的任务

b. 至于常说的核心和非核心线程,是人为的划分。只是创建的时机不一样。它们都是Worker!

c. 线程池也是一个对象,刚定义出来的线程池内部是空的!!就是说 workQueue 和 workers都是对应一个空对象。而不是一开始就根据传入的参数去先创建好线程--按需加载

2.2 工作过程--快速任务

(下面的分析基于:核心线程数为2,最大线程数为8,任务队列可容纳数为3)

打个比方

用户取餐

线程:外卖小哥

任务:送餐

任务队列:商家储餐柜

数据:餐

送一个餐,调过来一个外卖小哥(核心小哥),把餐给用户

送两个餐,再调过来两个外卖小哥(核心小哥),把餐给用户

送三个餐,先看那两个外卖小哥上一单送完没有。只要有一个送完了,就让他把这单送了;都没送完,把餐放到储餐柜,等这两个小哥送完的那个人,从储餐柜取出来,去送

送四个餐,还是先看那两个小哥上一单送完没有。逻辑同上

送五个餐,逻辑同上

送六个餐,这时候储餐柜满了,那就再调过来一个外卖小哥(非核心小哥、临时工),把餐给用户

最大线程数是8,其中有2个是核心小哥,那么临时工就是6位

也就是说,加上储餐柜的3个位置,可以同时给 8+3=11个用户送餐,

那送12个餐呢,这时候,要先看有没有小哥送完了,有空闲的小哥,把这单送了;都在忙,那走拒绝策略

默认的,抛异常--商家别接单了,忙不过来了。在送餐的小哥不受影响。但是后面只要有下单,就报错,当然了,有小哥送完了,还是能继续接单的。因为他是从储餐柜取的,他取走了,储餐柜就能继续放了

第二种策略,忽略新下的单--有小哥送完回来了,都是从储物柜拿出来去送。如果这时候有新下的单,储物柜有空就放在储物柜中,等着被拿走;如果储物柜还是满的,那就扔了...

第三种策略,忽略旧的单--有小哥送完回来了,都是从储物柜拿出来去送。如果这时候有新下的单,就是强硬的往储物柜放,原本在储物柜的单,扔掉...(小哥只管从储物柜拿就行了,他也不知道是老的还是新的单)

第四种策略,商家自己处理--商家自己决定,是不接单了,还是自己再雇人,还是自己给扔了...

当饭点过去了,没活了。当超时时间到了,还有活的接着干,闲着的临时工就被GC了。如果设置了允许核心线程超时,那么闲着的核心小哥也要被GC...      

         

验证开始

2.2.1 线程池工作,会调用 execute(Runnable),内部会调用任务(Runnable实现类)的run方法

第一个任务过来,会新建一个线程,放到 workers中,同时把任务放到 workQueue中,然后执行任务;

第二个任务过来,如果此时第一个任务还在执行,那么会再新建一个线程,同上放入后执行。

但是如果此时第一个任务已经执行完了,是会新建一个线程还是复用第一个任务的线程呢?

代码求证如下:

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.AbortPolicy()
    );

    @Test
    public void testCore() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 2; i++) {
            poolExecutor.execute(new JobFast("数据" + i));
            System.out.println("执行完毕");
        }

        System.out.println("全部结束");
    }

    static class JobFast extends Thread{
        private String data;

        public JobFast(String data) {
            this.data = data;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "在执行,操作:" + data);
        }
    }

在”执行完毕“ debug,最终可以看到下图:

分析:任务就打印了一句话,我们在后面debug的,第一个任务已经执行完毕了。结果第二个任务进入时,还是往workers中新建了一个worker。

结论:有新任务加入时,会新建worker。

然后把 for循环改成循环3次:大于核心线程数2!

 

 在“全部结束”处,debug,图如下:

 结合控制台的打印,如下:

结论:任务超出核心线程数的话,会把任务放入任务队列中;线程集合中的线程会复用(用线程1操作了数据3)

结合这两次实验,可以看出前面的结论:

2.2.3 第二个任务加入后,会直接新建线程来处理(这两个任务都不会放入任务队列)

2.2.4 第三个任务加入后,会把任务加入到任务队列中,复用之前的线程

2.2.5 第四个任务加入后,会怎么样呢?很简单,会加入到任务队列中,复用之前的线程--因为此时创建出来的线程是有能力处理这些任务的(任务队列能装下)

把for循环中 “执行完毕” 的打印删除--方便看控制台

2.2.6 第五个任务加入后,还是会复用--两个核心线程+任务队列中的任务数,控制台打印如下:

2.2.7 第六个任务加入后, 此时任务队列已经放不下了,但是还没达到最大线程数:8,

debug如下:

 控制台打印如下:

分析:点开debug的变量能发现,任务队列中的数据是:数据3,数据4,数据5(可能会不一样,但是肯定没有数据6!)

结论:任务队列放不下时,会再次新建线程,此时新建的线程就叫非核心线程,而对应的任务六被该线程直接处理

2.2.7 第十二个任务加入,这个线程池同时能够处理的任务数是:11 = 3 + 8(任务队列数+最大线程数),所以第九到第十一个任务都会创建非核心线程来处理--会创建6个非核心线程,而第十二个任务理论上来说,线程池处理不了,那么会走拒绝策略,我们使用的是默认拒绝策略:超过任务数抛异常。但是没有异常,控制台打印如下:

分析:这就是线程池的好处,因为前面的任务已经执行完毕了,是有空闲线程可以处理剩余的任务!!! 而且我们发现,只创建了5个线程=2个核心线程+3个非核心线程

2.3 工作过程--慢任务

修改下代码,让任务不要那么快执行完毕。

新定义一个JobSlow类--方法内部沉睡0.5秒;testSlow沉睡50秒是因为主线程结束,线程池就不继续工作了

新建一个Test模块跑同样的逻辑

    @SneakyThrows
    @Test
    public void testSlow() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 5; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

    static class JobSlow extends Thread {
        private String text;

        public JobSlow(String text) {
            this.text = text;
        }

        @SneakyThrows
        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "在执行,操作:" + text);
                Thread.sleep(500);
                return;
            }
        }
    }

 2.3.1 加入第五个任务后,控制台打印如下:

2.3.2 加入第六个任务后,控制台打印如下:

分析:和前面的结论一样,此时的任务数量还没有超过11

2.3.3 加入第十二个任务后,控制台打印如下:

分析:

a 创建了8个线程很好理解:最大线程数嘛

b 操作的数据,没打印出来数据3,4,5。为啥?因为携带这些数据的任务还在任务队列中呢!!

还没被消费掉,结果就因为任务数过多,走拒绝策略了

2.3.4 回到任务数十一来验证,控制台打印如下:

2.4 非核心线程复用

稍微修改下 快、慢任务类,打印中加个标识,然后用 5个慢任务把核销线程和任务队列占上,接着创建8个任务(非核心线程只有6个)

    @SneakyThrows
    @Test
    public void testAll() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 5; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        for (int i = 6; i <= 15; i++) {
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

    static class JobFast extends Thread {
        private String data;

        public JobFast(String data) {
            this.data = data;
        }

        @Override
        public void run() {
            System.out.println("【快】" + Thread.currentThread().getName() + "在执行,操作:" + data);
        }
    }

    static class JobSlow extends Thread {
        private String text;

        public JobSlow(String text) {
            this.text = text;
        }

        @SneakyThrows
        @Override
        public void run() {
            while (true) {
                System.out.println("【慢】" + Thread.currentThread().getName() + "在执行,操作:" + text);
                Thread.sleep(50000);
                return;
            }
        }
    }

打印结果如下:

 分析:

虽然有报错,但是还是能看出来是会复用的;

执行多次,会有那么几次不报错。所以应该是线程处理的问题了:还没校验当前任务总数是否达到最大任务数时,有些任务就已经执行完毕了。

2.5 过期时间

2.5.1 核心线程

线程池过期时间设置为2秒,每2.5秒执行一个任务,也就是加第二个任务开始,核心线程1会超过

过期时间(是否允许核心线程过期代码注释)

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.AbortPolicy()
    );    

    @SneakyThrows
    @Test
    public void testTimeOut() {
        System.out.println("------------- 线程池执行任务 -------------");
        //poolExecutor.allowCoreThreadTimeOut(true); //是否允许核心线程过期
        for (int i = 1; i <= 5; i++) {
            Thread.sleep(2500);
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

默认:不允许,控制台如下:

允许,控制台如下: 

分析:都有个方法了,不设置,肯定默认核心线程不受过期时间控制,设置后也能看出来,超过过期时间会重新创建线程。那么推导下,这个过期时间应该主要就是针对非核心线程了。

这里面有个东西:任务异常问题,修改如下:

    static class JobFast extends Thread {
        private String data;

        public JobFast(String data) {
            this.data = data;
        }

        @Override
        public void run() {
            System.out.println("【快】" + Thread.currentThread().getName() + "在执行,操作:" + data);
            throw new RuntimeException(); // 修改为异常任务
        }
    }


    @SneakyThrows
    @Test
    public void testTimeOut() {
        System.out.println("------------- 线程池执行任务 -------------");
//        poolExecutor.allowCoreThreadTimeOut(true); // 是否允许核心线程过期
        for (int i = 1; i <= 5; i++) {
            Thread.sleep(2500);
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

控制台如下:

能看出来,如果任务异常的话,整个线程池不会停止工作,而是会新建线程执行后续任务(允许核心线程异常注释掉了)

 2.5.2 非核心线程

根据上面的推导:过期时间主要就是对非核心线程的了

2.6 拒绝策略

慢任务修改为 沉睡2秒

2.6.1 默认 抛异常 AbortPolicy

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.AbortPolicy()
    );

    @SneakyThrows
    @Test
    public void testRejected() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 15; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        System.out.println("全部结束");
    }

控制台如下:

从上面能看到:任务数超过了,超过的部分不处理--数据,并且会抛异常(数据3、4、5在任务队列中,等着线程去处理呢)

2.6.2 静默丢弃、丢弃后续任务 DiscardPolicy

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.DiscardPolicy()
    );

    @SneakyThrows
    @Test
    public void testRejected() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 15; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }

        Thread.sleep(30000);
        System.out.println("全部结束");
    }

异常没有了,超出的任务也不处理了,但是会把任务队列中的任务-处理完

2.6.3 丢弃老任务 DiscardOldestPolicy

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.DiscardOldestPolicy()
    );

    @SneakyThrows
    @Test
    public void testRejected() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 15; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        Thread.sleep(30000);
        System.out.println("全部结束");
    }

 数据3、4、5、12没有处理,它们三个是最先放到任务队列中的。被丢弃了

2.6.4 自定义处理 CallerRunsPolicy

    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    @SneakyThrows
    @Test
    public void testRejected() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 15; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        Thread.sleep(30000);
        System.out.println("全部结束");
    }

只修改了线程池的拒绝策略

 所有的任务都执行了,然后数据12应该是要被丢弃的,但是主线程执行了

三、最后的结论

3.1 新任务到来会创建线程来处理任务,首先被创建出来的线程,被称为核心线程。

3.2 任务数量未达到核心线程,不会放入任务队列,而是继续创建线程(核心线程)来处理任务。

3.3 任务数量超出核心线程数的部分,会放入任务队列中,等待核心线程复用的时候,消费掉

3.4 任务队列放不下的话,这些超出的任务,会创建非核心线程来执行--不会放入任务队列,因为已经放不下了

3.5 任务数量超出了 最大线程数+任务队列数的话,后续任务会走拒绝策略

以上所有的东西,想表达的都是下图:

随着任务数量的不断增多,本来核心线程能处理的,变成要搭配上任务队列,再变成搭配上非核心线程,再变成搭配拒绝策略

四、所有的测试代码

package com.example.demo.thread;

import lombok.SneakyThrows;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootTest
@RunWith(SpringRunner.class)
public class ThreadTest {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
    ThreadFactory springThreadFactory = new CustomizableThreadFactory("自定义线程-");
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            8,
            2,
            TimeUnit.SECONDS,
            queue,
            springThreadFactory,
            new ThreadPoolExecutor.CallerRunsPolicy()
    );

    @Test
    public void testCore() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 12; i++) {
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
    }

    static class JobFast extends Thread {
        private String data;

        public JobFast(String data) {
            this.data = data;
        }

        @Override
        public void run() {
            System.out.println("【快】" + Thread.currentThread().getName() + "在执行,操作:" + data);
//            throw new RuntimeException(); // 修改为异常任务
        }
    }

    @SneakyThrows
    @Test
    public void testSlow() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 12; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

    static class JobSlow extends Thread {
        private String text;

        public JobSlow(String text) {
            this.text = text;
        }

        @SneakyThrows
        @Override
        public void run() {
            while (true) {
                System.out.println("【慢】" + Thread.currentThread().getName() + "在执行,操作:" + text);
                Thread.sleep(2000);
                return;
            }
        }
    }

    @SneakyThrows
    @Test
    public void testAll() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 5; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        for (int i = 6; i <= 15; i++) {
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

    @SneakyThrows
    @Test
    public void testTimeOut() {
        System.out.println("------------- 线程池执行任务 -------------");
//        poolExecutor.allowCoreThreadTimeOut(true); // 是否允许核心线程过期
        for (int i = 1; i <= 5; i++) {
            Thread.sleep(2500);
            poolExecutor.execute(new JobFast("数据" + i));
        }
        System.out.println("全部结束");
        Thread.sleep(50000);
    }

    @SneakyThrows
    @Test
    public void testTimeOut2() {
        System.out.println("------------- 线程池执行任务 -------------");
//        poolExecutor.allowCoreThreadTimeOut(true); // 是否允许核心线程过期
        for (int i = 1; i <= 5; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        for (int i = 6; i <= 11; i++) {
//            Thread.sleep(5000);
            poolExecutor.execute(new JobFast("数据" + i));
        }
        Thread.sleep(50000);
        System.out.println("全部结束");
    }

    @SneakyThrows
    @Test
    public void testRejected() {
        System.out.println("------------- 线程池执行任务 -------------");
        for (int i = 1; i <= 20; i++) {
            poolExecutor.execute(new JobSlow("数据" + i));
        }
        Thread.sleep(50000);
        System.out.println("全部结束");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LUNG108

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

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

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

打赏作者

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

抵扣说明:

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

余额充值