文章目录
概念理解
线程池是一种使用模式,它可以避免我们的程序频繁创建线程所消耗的时间和代价。通过线程池的方式,让已创建好的线程等待任务调用,并让执行完任务的线程重新等待。
优缺点
优点
即时等待任务的线程模式,提高了任务响应速度。
降低了创建线程的系统资源开销,充分利用了CPU核数,防止过分调度。
所有线程都呆在一起,方便了统一管理和调优。
缺点
线程的创建和销毁是影响线程池性能的一个重要因素
创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
销毁太多线程,将导致之后浪费时间再次创建它们。
创建线程太慢,将会导致长时间的等待,性能变差。
销毁线程太慢,导致其它线程资源饥饿
所以对于线程池中的线程数的确定是一个关键
根据场景确定线程数(普通)
Java中运行时核数计算代码
System.out.println(Runtime.getRuntime().availableProcessors());
针对不同场景,需要不同的线程数
场景可以分为以下三种
-
CPU密集型
比较占用CPU资源的场景,尽量减少线程数量,因为过多的线程会导致过多的抢占cpu资源,引起不必要的上下文切换。
线程池大小设置为 运行时核数+1 -
IO密集型
需要执行长时间的IO操作的,那么对于CPU资源是不怎么占用的,那么可以设置多一点的线程数。当一部分线程在等待IO的时候,剩下的线程可以获取CPU资源去做一些其它的事情,这样并行效率就能提高,CPU资源利用率变大。
线程池的大小设置为 2*运行时核数+1
还有一种是 运行时核数/(1-阻塞系数) -
混合密集型
对于混合密集型,即CPU和IO都需要,这个时候,我们可以通过通用公式来计算线程的数量。
线程池的大小设置为 运行时核数 * (1+ IO 耗时/CPU 耗时)
但还是要经过实际测试来确定更为准确的线程池线程数量
已有线程池 (4大方法)
-
Executors.newSingleThreadExecutor();
如下图所示,创建一个核心线程数和最大线程都为1的线程池。
阻塞队列为链表形式
-
Executors.newFixedThreadPool(int nThreads);
如下图所示,创建一个固定大小的线程池,核心线程数和最大线程数为你传入的参数 。
阻塞队列为链表形式
-
Executors.newCachedThreadPool();
如下图所示,可伸缩线程池,创建一个核心线程数为0,最大线程数是2的31次方。这样你有多少任务,我就为你开多少个线程。当然不可能开到那么多,受你的电脑配置的影响。最大你能开多少个与你电脑有关。
阻塞队列为同步队列
-
Executors.newScheduledThreadPool(int corePooSize);
该方法其实调用的是ScheduledThreadPoolExecutor类下的方法。
如下图所示,创建一个核心线程数为传入参数,最大线程数是Integer.MAX_VALUE的线程池。
阻塞队列为延迟队列,故可以用来实现延时执行任务
自定义线程池(7大参数)
对比上面几种已有线程池的创建,可以发现,最后都是通过ThreadPoolExecutor来创建线程池,所以我们也可以通过自定义线程池的方式来创建适合自己项目的线程池
从上至下的参数意思依次为
1.核心线程数
2.最大线程数
3.生存时间
4.时间单位
5.阻塞队列大小
6.创建线程使用的工厂
7.拒绝策略
拒绝策略 (4大策略)
从上至下依次是
1.AbortPolicy 总是抛出异常
2.CallerRunsPolicy 将任务丢给启动线程池的线程去执行。
3.DiscardOldestPolicy 让最早进入阻塞队列的离开,然后自己进去排队
4.DiscardPolicy 直接丢弃任务
拒绝策略的使用场景
- AbortPolicy
无特殊使用场景,默认就是这个拒绝策略。对于一些比较重要的业务,可以使用该拒绝策略,方便出错的时候即时发现错误原因 - DiscardPolicy
适用于不太重要的业务场景,不抛出错误,比如博客的阅读量这种的 - DiscardOldestPolicy,将最早进入阻塞队列的丢弃,典型的喜新厌旧,看你是不是对于老的任务需要。
- CallerRunsPolicy
将任务丢给线程池本身的线程去运行,一般在不允许失败的、对性能要求不高、并发量较小的场景下使用。不然的话,容易降低性能
ThreadPoolExecutor的关键参数 ctl 和 运行时状态
来看ThreadPoolExecutor源码给出的描述
可以发现 ctl 即代表一个池中的控制状态(control state)。是一个原子整型类,其中封装了两个字段。
一个是workerCount,代表线程池运行的数量
一个是runState,代表线程池运行的状态
那么,一个原子整数类型的ctl,是怎么做到既能表示线程数目,又能表示线程状态的呢?
可以发现,我们的字段runState是用高三位(最前面一位用来标识正负数)来表示的,剩余的位数用来表示线程池数量
剩余的位数,取决于具体的Integer的位数-3,这里是4字节,所以32位
通过下面程序可计算查看二进制值。
public static void c(Integer a){
System.out.println(Integer.toBinaryString(a));
}
public static void main(String[] args) {
c( -1<<29 );
}
RUNNING -1 111......00000000000
SHUTDOWN 0 000......00000000000
STOP 1 001......00000000000
TIDYING 2 010......00000000000
TERMINATED 3 011......00000000000
那么,对于不同的状态,线程池是怎么处理当前线程的呢
同样可以在源码开头的注释中,找到说明
RUNNING :处理新任务,并处理队列中的任务
SHUTDOWN :不接受新的任务,但会处理好阻塞队列中剩余的任务
STOP :不接受新的任务,不处理阻塞队列中的任务,并且中断正在执行中的任务
TIDYING :所有的任务都结束了,运行的线程数也为0了,那么就进入TIDYING状态,将会运行terminated()方法
TERMINATED :当terminated()方法执行完,则线程进入此状态
那么,不同的状态之间是怎么进行转换的呢?
RUNNING 转为 SHUTDOWN : 当显示执行shutdown方法时,或者在使用finalize()方法中被隐式执行
RUNNING和SHUTDOWN 转为 STOP :当显示执行shutdownNow()方法时
SHUTDOWN 转为 TIDYING :当阻塞队列和线程池都为空
STOP 转为 TIDYING : 当线程池为空
TERMINATED :当terminated()方法执行完
IO耗时和CPU耗时怎么计算
1、可以将你要使用线程池的业务代码,对其中的业务,计算cpu执行耗时和io耗时。
2、使用专业的工具计算,例如 APM工具