线程池

基本概念

什么是线程池?

线程池其实就是一种多线程处理的方式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务.这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过了的实现了Runnable或Callable接口的实例对象;

为什么使用线程池?

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力.

使用线程池有什么优势?

  1. 线程和任务分离,提高线程重用性
  2. 控制线程并发数量,降低服务器压力,统一管理所有线程
  3. 提升系统响应速度,假如创建线程用的时间是T1,执行任务的时间是T2,摧毁线程的时间T3, 那么使用线程池就免去T1和T3的时间;

工作原理

ThreadPoolExecutor

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,  //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //最大空闲时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //任务队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) //饱和处理机制

自定义线程池

参数涉及分析

  • 核心线程数

    核心线程数的涉及需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么想要在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所有我们一般按照8020原则设计即可,即按照80%的情况设计核心线程数,剩下的20%可以利用最大线程处理

  • 任务队列长度

    任务队列长度一般的设计为:核心线程数/单个任务执行时间*2;

    例如上面的场景,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200

  • 最大线程数

    最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:

    例如 如果每秒产生最大的任务是1000个,那么

    *最大线程数=(最大任务数-任务队列长度)单个任务执行时间

  • 最大空闲时间

    这个参数的设计完全参照系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生的任务时间的间隔合理设置一个值即可.

自定义线程池实现步骤

  1. 编写任务类 实现Runnable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyTask implements Runnable {
private int id;
//由于run方法重写接口中的方法 id属性初始化可以利用构造方法完成
public MyTask(int id){
this.id=id;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("线程:"+name+"即将执行任务:"+id);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace()
}
System.out.println("线程:"+name+"完成了任务:"+id);
}
@Override
public String toString() {
return "MyTask{" +
"id=" + id +
'}';
}
}
  1. 编写线程类,用于执行任务 需要持有所有任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyWorker extends Thread {
//保存线程名字
private String name;
//保存所有名字
private List<Runnable> tasks;

public MyWorker(String name, List<Runnable> tasks) {
this.name = name;
this.tasks = tasks;
}

@Override
public void run() {
//判断集合中是否有任务 只要有任务 就一直执行
while(tasks.size()>0){
Runnable r = tasks.remove(0);
r.run();
}
}
}
  1. 编写线程池类,包含提交任务 执行任务的能力
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
37
38
39
40
41
42
43
public class MyThreadPool {
//任务队列 集合 需要控制线程安全
private List<Runnable> tasks = Collections.synchronizedList(new LinkedList<Runnable>());
//当前线程数量
private int num;
//核心线程数量
private int corePoolSize;
//最大线程数量
private int maxSize;
//任务队列长度
private int workSize;

public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maxSize = maxSize;
this.workSize = workSize;
}

//1.提交任务:
public void submit(Runnable r){
//判断当前集合中任务数量是否大于最大任务数量
if (tasks.size()>=workSize){
System.out.println("任务:"+r+"被丢弃了");
}else{
tasks.add(r);
execTask(r);
}
}
//专门用于执行任务
private void execTask(Runnable r) {
//判断当前线程池中的线程总数量是否超出了核心数
if (num<corePoolSize){
new MyWorker("核心线程:"+num,tasks).start();
num++;
}else if (num<maxSize){
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else{
System.out.println("任务:"+r+"被缓存了");
}
}
}

  1. 编写测试类,创建线程池对象,提交多个任务测试
1
2
3
4
5
6
7
8
9
10
11
public class MyTest {
public static void main(String[] args) {
//创建线程池类对象;
MyThreadPool pool = new MyThreadPool(2, 4, 20);
for (int i = 0; i < 10; i++) {
//生成一个任务,提交给线程池
MyTask myTask = new MyTask(i);
pool.submit(myTask);
}
}
}

Java内置线程池

ExecutorService接口是java内置的线程池接口 通过学习接口中的方法 可以快速的掌握java内置线程池的基本使用

  • void shutdown()

    启动一次顺序关闭 执行以前提交的任务 但不接受新任务

  • List<Runnable> shotdownNow()

    停止所有正在执行的任务,暂停处理正在等待的任务 并返回等待执行的任务列表

  • <T> Future <T> submit(Callable<T> task)

    执行带返回值的任务 ,返回一个Future对象

  • Future<?> submit(Runnable task)

    执行Runnable任务,并返回一个表示该任务的Future

  • <T> Future <T> submit(Runnable task,T result)

    执行Runnable任务,并返回一个表示该任务的Future

ExecutorService 获取

image-20210314130235348

弊端

newFixedThreadPool和newSingleThreadExecutor:

主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM

newCachedThreadPool和newScheduledThreadPool

主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM

SchduledExecutorService

image-20210314153208256

异步计算结果(Future)

image-20210314153519310

使用Java内置线程池完成综合案例

拒绝策略

  1. AbortPolicy: 默认策略,在需要拒绝任务时抛出
  2. CallerRunsPolicy: 直接在execute方法的调用线程中运行被拒绝的任务,如果线程池已经关闭,任务将被丢弃
  3. DiscardPolicy: 直接丢弃任务
  4. DiscardOldestPolicy: 丢弃队列中等待时间最长的任务,并执行当前提交的任务, 如果线程池已经关闭,任务被丢弃