学习线程池的时候,要对线程有一定理解
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
池化(pool)技术的本质是通过复用对象、连接等资源,减少创建对象/连接,降低垃圾回收(GC)的开销,适当使用池化相关技术能够显著提高系统效率,优化性能。
程序的运行,本质:占用系统的资源! 优化资源的使用!
线程的创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
我对线程池的理解:
众所周知,我们在创建线程的时候,需要new一个Thread的对象,这样每次都创建一个对象,不仅会消耗不必要的资源,而且如果并发量特别高的话,线程就会创建得特别的多,会造成OOM(内存溢出)的问题
那么线程池可以解决这些问题,假设(我这里为了方便初学的同学,可以先这样理解着),我们创建一个有10个线程的线程池放在那,外部请求来了,会调用线程池内部的10个线程,每个线程执行完自己的任务,会跑回线程池待着,等待下一个任务的调用,省去了创建线程的时间,并且如果并发量高了的话,我们会禁止其他请求的访问,就不会一直重新开线程,造成OOM的问题了
线程池的优势:线程复用、可以控制最大并发数、管理线程
总体来说,线程池有如下的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池讲解:三大方法、七大参数、四种拒绝策略
七大参数:
创建线程池,最核心的方法就是ThreadPoolExecutor
这个类的这个构造方法,这个方法有7个参数是特别重要的。
public ThreadPoolExecutor(int corePoolSize,// 核心线程池大小
int maximumPoolSize,// 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit,// 超时单位
BlockingQueue<Runnable> workQueue,// 阻塞队列
ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handler) { // 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
讲解:
-
corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
-
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
-
keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
-
unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
-
workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
-
threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
-
handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
我的图解:核心线程数为4,最大线程数为8,队列为5
1、刚开始创建的时候:
2、当任务小于核心线程数的时候:来了四个任务
这四个任务先进任务队列,核心线程有空着的,就拿任务来用
3、当任务小于任务队列+核心线程数的时候:来了八个任务
4、当任务大于任务队列+核心线程数的时候:来了11个任务
(1)、先进任务队列,结果任务队列不让进,就去尝试最大线程数
(2)、最大线程数没满,线程池就开辟新的线程(使用线程工厂),他们就插队先执行
5、当任务大于最大线程数+任务队列的时候:来了15个任务
这里也是先进任务队列,结果进不了,再看最大线程数,结果最大线程数也满了,就要用到拒绝策略了
6、当没任务了:0个任务
7、过了KeepAliveTime后,线程池会回到原点
三大方法:(工作不能用,但必须了解)(阿里开发手册中强制规定不能用)(为啥不能用,因为在高并发的情况下,你的服务器会因为这个线程池而“爆掉”)
这三大方法是基于java.util.concurrent
包(JUC)下面的Executors
这个工具类来调用的
1、ExecutorService threadPool = Executors.newSingleThreadExecutor(); 单线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
//根据刚才我讲解的不难看出,这里只有一个线程,没有拒绝策略,而是用任务列表,一直加
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
在高并发的情况下,会造成的问题,他一直往队列中加任务,会造成OOM(内存溢出),简单来说就是你内存不够了,直接卡死
2、ExecutorService threadPool = Executors.newFixedThreadPool(5);固定线程个数
public static ExecutorService newFixedThreadPool(int nThreads) {
// 这里我们是自己定义了一个核心线程数,与最大线程数,不难看出非核心线程数(最大线程数-核心线程数)根本就没有,如果超出了,没有拒绝策略,也是用任务队列一直加
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
在高并发的情况下,会造成的问题,他一直往队列中加任务,会造成OOM(内存溢出),简单来说就是你内存不够了,直接卡死
3、ExecutorService threadPool = Executors.newCachedThreadPool();缓存池,可扩展
public static ExecutorService newCachedThreadPool() {
// 这里没有核心线程数,但有大量的非核心线程数(最大线程数-核心线程数)
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
高并发的情况下,会造成CPU效率100%,从而烧掉CPU
四种拒绝策略(对应四个实现类)
- AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行
- CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
- DiscardPolicy - 直接丢弃,其他啥都没有,不会抛异常
- DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
我的理解(白话文):
- AbortPolicy :丢掉任务,并抛异常
- CallerRunsPolicy :任务将由调用者线程去执行。就是说你如果是主函数调用的,多的任务会由主函数执行
- DiscardPolicy:丢掉任务,不抛异常
- DiscardOldestPolicy :抛弃进入任务队列最早的那个任务,然后尝试把这次的任务放入任务队列
线程池参数调优:没有最优的,要按实际情况改,最后附我最常有的参数
既然不能用
Executors
去设定线程池,那么我们就要自己去设置线程池的大小,怎样才能得到最优的值呢?
一、了解:IO密集型,CPU密集型
CPU密集型(CPU bound)
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),
I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。
例如,一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
IO密集型(I/O bound)
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
CPU密集型 vs IO密集型
我们可以把任务分为计算密集型和IO密集型。
CPU密集型
一般来说:计算型代码、Bitmap转换、Gson转换等
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。
这种计算密集型任务虽然也可以用多任务完成,但是,任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,
所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。
Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型
一般来说:文件读写、DB读写、网络请求等
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,
任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。
对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
总之,计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。
调优参数:
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
获取CPU的核数
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
我的线程池(单例模式,让这个线程池全局唯一,当然你用反射破解了我也无话可说):
/**
* 线程池工具类,单例模式,全局唯一
* @author ZHAOPINGAN
*/
public class ThreadPoolUtil {
private static volatile ExecutorService threadPool = null;
private ThreadPoolUtil(){}
public static ExecutorService getThreadPool() {
if (threadPool == null) {
synchronized (ThreadPoolUtil.class) {
if (threadPool == null) {
threadPool = new ThreadPoolExecutor(
// 核心线程数
4,
// 最大线程数
10,
// 10s 过期时间
10000L, TimeUnit.MILLISECONDS,
// 队列 1000
new LinkedBlockingQueue<>(1000),
// 线程池工厂
Executors.defaultThreadFactory(),
// 拒绝策略 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息
new ThreadPoolExecutor.AbortPolicy()
);
}
}
}
return threadPool;
}
}