目录
一、写在前面
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("全部结束");
}
}