线程池作用
思考线程有什么弊端?
- 线程在java中是一个对象,更是操作系统的宝贵资源,线程创建和销毁都需要时间。如果创建时间+销毁时间 >执行时间就很不划算
- java对象占用堆内存,操作系统线程占用系统内存。根据JVM规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存
- 操作系统需要频繁切换线程上下文(所有线程都想被运行)影响性能
线程池的作用就是为了控制线程数量,管理线程生命周期
线程池原理-概念
线程池管理器
- 用于创建并管理线程池,包括创建线程,销毁线程,添加新任务
工作线程
- 线程池中线程,在没有任务时处于等待状态,可以循环的执行任务
任务接口
- 每个任务都必须实现的接口,以供工作线程调度任务的执行,它主要规范了任务的入口,任务执行完后的收尾工作,任务的执行状态等
任务队列
- 用于存放没有处理的任务,提供一种缓存机制
线程池API简介
类型 | 名称 | 描述 |
---|---|---|
接口 | Executor | 最上层的接口,定义了执行任务的方法execute |
接口 | ExecutorService | 继承了Executor接口,扩展了Callable、Future、关闭方法 |
接口 | ScheduledExecutorService | 继承了ExecutorService接口,扩展了定时任务相关方法 |
实现类 | ThreadPoolExecutor | 基础、标准的线程池实现 |
实现类 | ScheduledThreadPoolExecutor | 继承了ThreadPoolExecutor实现了ScheduledExecutorService接口中相关定时任务的方法 |
可以认为ScheduledThreadPoolExecutor是最丰富的实现类
ExecutorService
//监测ExecutorService是否已经关闭,直到所有任务完成执行,或超时完成,或当前线程被中断
executorService.awaitTermination(long timeout, TimeUnit unit);
//执行给定的任务集合,执行完毕后,返回结果
executorService.invokeAll(Collection<? extends Callable<T>> tasks);
//执行给定的任务集合,执行完毕或者超时后,返回结果,其他任务终止
executorService. invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit);
//执行给定的任务,任意一个执行成功则返回结果,其他任务终止
executorService.invokeAny(Collection<? extends Callable<T>> tasks);
//执行给定的任务,任意一个执行成功或者超时则返回结果,其他任务终止
executorService.invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit);
//如果此线程池已关闭,则返回true
executorService.isShutdown();
//如果关闭后所有任务都已完成,则返回true
executorService.isTerminated();
//优雅关闭线程池,之前提交的任务将被执行,但是不会接收新的任务
executorService.shutdown();
//尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表
executorService.shutdownNow();
//提交一个用于执行的Callable返回任务,并返回一个Future<T>对象,用于获取Callable执行结果
executorService.submit(Callable<T> task);
//提交可运行任务执行,并返回一个Future对象,执行结果为null
executorService.submit(Runnable task);
//提交可运行任务执行,并返回一个Future对象,执行结果为传入的result
executorService.submit(Runnable task,T result);
ScheduledExecutorService
在ExecutorService定义的API基础上扩展了以下几个定时相关的API
//创建并执行一个一次性有返回值的任务,过了延迟时间就会被执行
scheduledExecutorService.schedule(Callable < V > callable, long delay, TimeUnit unit);
//创建并执行一个一次性的任务,过了延迟时间就会被执行
scheduledExecutorService.schedule(Runnable command, long delay, TimeUnit unit);
//创建并执行一个周期性的任务,过了初始延迟时间第一次被执行,后续以给定的周期时间执行。执行过程中发生了异常,那么任务就停止
//一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和scheduleWithFixedDelay的重要区别
scheduledExecutorService.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
//创建并执行一个周期性的任务,过了初始延迟时间第一次被执行,后续以给定的周期时间执行。执行过程中发生了异常,那么任务就停止
//一次任务执行时长超过了周期时间,下一次任务会在该次任务执行结束时间的基础上,计算执行延时。对于超过周期的长时间处理任务的不同处理方式,这是它和scheduleAtFixedRate的重要区别
scheduledExecutorService.scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
特别注意scheduleAtFixedRate和scheduleWithFixedDelay的区别,其实对于正常执行的情况来说他们没有区别,可是对于任务执行时间 > 周期任务的调度时间他们就有不同的处理方式
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.err.println("scheduleAtFixedRate开始执行"+ LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("scheduleAtFixedRate执行本次任务花了3秒,影响了每隔2秒执行一次的周期任务.得立刻调度周期任务"+ LocalDateTime.now());
}, 1, 2, TimeUnit.SECONDS);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println("scheduleWithFixedDelay开始执行"+ LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("scheduleWithFixedDelay执行本次任务花了3秒,影响了每隔2秒执行一次的周期任务.从现在开始计算到了周期时间(2秒)后再调度周期任务"+ LocalDateTime.now());
}, 1, 2, TimeUnit.SECONDS);
scheduleAtFixedRate如果执行任务的时间 > 周期任务的调度时间任务执行结束后会立刻调度新的周期任务
scheduleWithFixedDelay如果执行任务的时间 > 周期任务的调度时间任务执行结束后会在当前时间的基础上重新计算调度新的周期任务时间
Executors工具类
可以自己实例化线程池,也可以使用Executors创建线程池的工厂类,常用方法如下
//创建一个固定大小,任务队列容量无界的线程池.核心线程数=最大线程数
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
//创建一个大小无界的缓存线程池,它的任务队列是一个同步队列.任务加入到线程池中,如果线程池中有空闲线程,则用空闲线程执行,如无则创建新线程执行.
//线程池中的线程空闲超过60秒,将被销毁释放.线程数随任务的多少变化.适用于执行消耗较小的异步任务.核心线程数=0,最大线程数量=Integer.MAX_VALUE
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//只有一个线程来执行无界任务队列的单一线程池.该线程池确保任务按加入的顺序一个一个依次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续的任务
//与newFixedThreadPool(1)的区别在于单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能再改变
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
//创建一个可以执行定时任务的线程池.该池的核心线程数由参数指定,最大线程数Integer.MAX_VALUE
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
Executors工具类的队列都是无界的,这可能会导致程序内存溢出,所以尽量自己实例化线程池,具体原因得看线程池execute内部的实现原理
线程池execute内部实现原理
如果没有搞懂execute的内部原理,线上环境很可能会出现内存溢出
// 线程池信息:核心线程数量5,最大线程数量10.无界队列.超出核心线程数量的线程存活时间:5秒
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
for (int i = 0; i < 15; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行结束" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("任务提交成功" + i);
}
//查看线程数量,查看队列等待数量
TimeUnit.SECONDS.sleep(1);
System.err.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.err.println("当前线程池等待数量为:" + threadPoolExecutor.getQueue().size());
//等待15秒,查看线程数量和队列数量(理论上,会被超出核心线程数量的线程执行)
TimeUnit.SECONDS.sleep(9);
System.err.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.err.println("当前线程池等待数量为:" + threadPoolExecutor.getQueue().size());
从执行结果来看,15个任务已经提交到线程池.但是处理任务的线程始终只有5个,并没有根据最大线程数加开线程
难道设置的最大线程数是摆设的吗?为什么会没有加开线程呢?
带着问题点来看下ThreadPoolExecutor.execute的源码是如何实现的
可以看到官方也给出了注释,简单来说分为三步
- 判断线程池中线程的存活数量是否小于核心线程数量,如果小于加开线程处理该任务
- 超过核心线程数量加入任务队列,加入成功等待线程池中存活线程处理
- 加入队列失败判断线程的存活数量是否小于最大线程数数量如果小于加开线程处理该任务,否则拒绝执行
总结
我: 关于这一点颠覆了之前对线程池的认知,我觉得只要超过核心线程数再有任务进来你会加开线程
线程池: 我不要你觉得我要我觉得,只要任务队列能一直缓存我就不会加开新的线程
线程池的使用
无界队列-线程池
// 创建一个核心线程数=5,最大线程数=10,任务队列=3的线程池,指定自己的拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10,
5, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
// 执行十五个任务
for (int i = 0; i < 15; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行结束" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.err.println("当前线程池的线程数量为:" + threadPoolExecutor.getPoolSize());
System.err.println("当前线程池的任务数量为:" + threadPoolExecutor.getQueue().size());
threadPoolExecutor.shutdown();
所有任务都执行完毕,看起来是个很不错的线程池,可是细心的小伙伴就看出来了当前线程池的线程数量始终都是核心线程数,最大线程数量丝毫不起作用。因为最大线程数量起不起作用取决于任务队列是否还能继续容纳任务,如果容纳不了才会加开线程处理。所以对于无界队列线程池来说,需要注意的是当任务堆积到很大量的时候,由于线程消费不过来导致队列可能会内存溢出
有界队列-线程池
// 创建一个核心线程数=5,最大线程数=10,任务队列=3的线程池,指定自己的拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10,
5, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(3), (r, executor) -> System.err.println("有任务被拒绝执行了"));
// 执行十五个任务
for (int i = 0; i < 15; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行结束" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.err.println("当前线程池的线程数量为:" + threadPoolExecutor.getPoolSize());
System.err.println("当前线程池的任务数量为:" + threadPoolExecutor.getQueue().size());
threadPoolExecutor.shutdown();
有两个任务被拒绝执行了因为线程池最大容纳的任务数量为最大线程数+任务队列数,所以图上线程池最大容纳的任务是10+3=13,可是提交了15个任务,15-13 = 2 所以会有两个任务被拒绝执行了
当前线程池的线程数量为10是因为线程池最大数量设置的是10,所以最多只有十个线程处理任务。当没有任务时会根据keepAliveTime释放加开的线程
有界队列需要特别注意的是 当任务数超过了线程池最大容纳数时,需要采取怎么样的拒绝策略
缓存-线程池
// 创建一个核心线程数=5,最大线程数=10,任务队列=3的线程池,指定自己的拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60, TimeUnit.SECONDS,
new SynchronousQueue<>());
// 执行十五个任务
for (int i = 0; i < 15; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行结束" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.err.println("当前线程池的线程数量为:" + threadPoolExecutor.getPoolSize());
System.err.println("当前线程池的任务数量为:" + threadPoolExecutor.getQueue().size());
threadPoolExecutor.shutdown();
缓存线程池最大的特点是自动加开和自动回收线程.当没有任务时会根据keepAliveTime释放线程,因为核心线程数为0所以会释放掉所有的线程,当有任务时看下有没有正在空闲的线程,如果有则复用,没有则加开
SynchronousQueue实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他线程池不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。在使用SynchronousQueue作为工作队列的前提下客户端代码向线程池提交任务时。而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务.那么相应的offer方法就会调用失败(既任务没有存入队列).此时ThreadPoolExecutor会新建一个工作线程用于对这个入队失败的任务进行处理,前提建立在没有达到最大线程数量上。
个人比较推荐使用缓存线程池,因为任务来了,不想堆积也不想拒绝只想快速处理掉,所以使用这种线程池能快速的处理完任务.但是主要注意的地方是不要将maximumPoolSize设置为Integer.MAX_VALUE.因为线程数量太大,CPU也会卡顿,建议设置成一千甚至一万也不要给一个无限大的值.
定时任务-线程池
一次性任务
// 定时线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);
AtomicInteger atomicInteger = new AtomicInteger();
scheduledThreadPoolExecutor.schedule(() -> {
atomicInteger.getAndAdd(1);
System.out.println("第" + atomicInteger.get() + "次执行");
}, 1, TimeUnit.SECONDS);
设置delay和unit的值确定好执行时间,一个任务就会执行一次,一次性任务使用比较简单,几乎没什么套路.但是有个地方是需要注意的,如果说线程池在某个时间点特别多任务的时候,执行任务的时间就会有延迟.
举个例子: 线程池数量设置的是5,一秒后执行,但是同一时间提交上来的任务有100个,这时候最多只会有五个线程处理这100个任务,所有得等着一个个处理完.并不是一秒后立刻执行完100个任务.
再提一下定时任务执行的本质,它的本质就是一个延时队列(DelayedWorkQueue),也就是将任务放到一个延时队列.所谓的延时队列的基本概念就是一个数据加入队列以后没有到时间点从里面取不出来,到了时间点才能从里面取出来.
周期性任务
// 定时线程池
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);
//效果1:提交后,2秒后开始第一次执行.之后每间隔1秒执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)
//也就是说这个代码中是,3秒执行一次(计算方式:每次执行3秒,每隔时间1秒,执行结束后马上开始下一次执行,无需等待)
scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务1被执行.现在时间:" + System.currentTimeMillis());
}, 2, 1, TimeUnit.SECONDS);
//效果2:提交后,2秒后开始第一次执行.之后每间隔1秒执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒)
//也就是说这个代码执行的效果是4秒钟执行一次(计算方式:每次执行3秒,每隔时间1秒,执行完以后再等待1秒,所以是3+1)
scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("任务2被执行.现在时间:" + System.currentTimeMillis());
}, 2, 1, TimeUnit.SECONDS);
周期性任务分为两种执行方式,对应代码中的效果1和效果2.这个强调的一点就是,无论执行任务的时长是否大于周期时长都不会出现并行执行而始终都是串行执行.但是对于任务处理时长大于周期时长
效果1(scheduleAtFixedRate)等待完毕,完毕后立刻执行
效果2(scheduleWithFixedDelay)等待完毕,等上一次执行完毕后再开始计时,等待1秒
终止线程池
shutdown
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10,
5, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3));
for (int i = 0; i < 5; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> System.out.println("线程执行"+ finalI));
}
//关闭线程池,不再接受新任务
threadPoolExecutor.shutdown();
Thread.sleep(1000);
threadPoolExecutor.execute(() -> {
});
shutdown需要注意的点是调用之后,会把调用之前的任务全部处理完,才关闭线程池.而在调用shutdown之后,是无法再提交任务到线程池的,直接进入线程池的拒绝策略.
shutdownNow
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10,
5, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3));
for (int i = 0; i < 10; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> System.out.println("线程执行" + finalI));
}
//立刻关闭线程池,返回没有处理完的任务列表
List<Runnable> runnables = threadPoolExecutor.shutdownNow();
System.out.println(runnables.size());
shutdownNow需要注意的点是调用之后会立刻关闭线程池,即使存活线程在运行状态,也尝试着去终止线程.并返回没有处理完的任务列表,同样在调用shutdownNow之后,是无法再提交任务到线程池的,直接进入线程池的拒绝策略.
如何确定合适的线程数量
前面说到线程池存在的目的是为了管理线程的数量,那么这个数量到底是为多少才算合适.先确定线程执行什么任务
计算型任务
计算型任务指的是一些纯内存的操作,比如加减乘除运算或计算哈希值等, 这种属于CPU密集型任务
设置为CPU数量的1-2倍,比如说CPU是8核那么设置为16就好了
IO型任务
比如tomcat,数据库连接池,这些都属于IO型任务
相比于计算型任务,需要多一些线程,要根据具体的IO阻塞时长进行考量决定.如tomcat中默认的最大线程数为200.
也可以考虑使用缓存线程池,根据需要在一个最小数量和最大数量间自动增减线程数
小窍门
可以通过监控CPU的情况来分析服务器是否已经充分使用了,在生产环境中CPU的应用率达到80%就可以说是达到一个充分利用了
如果说CPU使用率小于80%那么在CPU应用这一块是不合理的
如果说CPU使用率太满了可能是线程数量太多了导致CPU处理不过来