线程池到底是什么?
为什么要了解线程池?
首先面试中经常被问到的线程池,其次因为工作中可能你并不仅仅听到线程池,有可能更多的是数据库连接池、内存池、对象池等等的一些列“池”。万变不离其宗,了解了线程池也就是对其他的有一些了解。那线程池到底是什么玩意?本文带你一步步探索,包括线程池的三大方法、七大参数、四种拒绝策略
程序的运行本质:占用系统的资源。
那么为了优化系统资源的使用就需要用到池化技术!
池化技术
池化技术就是构建类似于池子一样的东西来存放资源,有人(线程)要用就拿去用,用完再放回来。避免了用的时候创建,不用销毁,再用再创建… 因为创建和销毁是非常消耗和浪费资源的(可以理解为耗时耗力)那么系统层面一旦耗时耗力,那么对于用户影响的肯定是响应速度了。
线程池的优势
- 降低资源的消耗
- 提高响应速度
- 方便线程管理
线程可复用、控制最大并发数、管理线程
三大方法
/**
* 创建单一线程的线程池
*/
ExecutorService service = Executors.newSingleThreadExecutor();
/**
* 创建固定个数线程的线程池
*/
ExecutorService service1 = Executors.newFixedThreadPool(5);
/**
* 创建可缓存线程的线程池
*/
ExecutorService service2 = Executors.newCachedThreadPool();
newSingleThreadExecutor
池中只有一个线程执行任务,用这个线程一个个执行任务
示例:
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
service.shutdown();
}
}
可以看到,始终是这一个线程pool-1-thread-1
在执行任务。
newFixedThreadPool
固定数量线程的线程池,需要指定线程个数,用指定数的线程来执行任务。
示例:
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5); //指定线程个数
try {
for (int i = 0; i < 10; i++) {
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
service.shutdown();
}
}
可以看到,这次有多个线程在执行任务
newCachedThreadPool
可缓存的线程池,不用指定线程数,根据执行情况增加线程,当线程执行完毕回回到池中,看源码发现有线程存活时间(超时会销毁)
示例:
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
service.shutdown();
}
}
上图所示,执行任务已经调用了7个线程了,实际上多次运行的结果也不太相同有可能池中创建更多或更少的线程来完成任务。
七大参数
我们都知道创建线程池可以用Excutors
工具类,但是阿里巴巴开发手册里明确写了创建线程池要用原生的带参数的方式创建以便开发者可以看出线程池内部的参数设置,避免资源耗尽风险。可以在IDE中安装插件检查代码规范,这里不在赘述。
源码查看:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
发现三种线程池的都用了ThreadPoolExecutor
这个方式去创建线程,我们就要研究一下这个方法,继续点源码发现它调用this
,继续点进去
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
这里就发现了七大参数
- corePoolSize:核心线程个数
- maximumPoolSize:最大允许线程个数
- keepAliveTime:需要销毁的线程的存活时间,超时释放
- TimeUnit unit:时间单位
- BlockingQueue workQueue:阻塞队列
- ThreadFactory threadFactory:线程工程(用于创建线程的)
- RejectedExecutionHandler handler:拒绝策略
下面通过生活场景来模拟七大参数的意思
银行正在办理的窗口有两个,关闭的窗口三个,等待区有三个座位,这时候有两个人(待执行任务)来办理业务。
对应的线程池构建如下:
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2,//corePoolSize核心运行线程数,相当于正在办理业务的柜台
5,//maximumPoolSize最大线程数,相当于柜台总数
3,//keepAliveTime每个柜台的工作时间
TimeUnit.SECONDS,//TimeUnit unit单位秒
new LinkedBlockingQueue<>(3),//阻塞队列容量3,相当于等待区的3个座位
Executors.defaultThreadFactory(),//系统默认创建线程工厂
new ThreadPoolExecutor.AbortPolicy());//拒绝策略,下面会讲
下面我们来看看这个线程池具体如何执行的:
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <=2; i++) {
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "=> OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
}
我们设置两个任务去执行
很明显,两个柜台正在办理这两个业务,也就是两个核心线程正在执行任务。
两个柜台正在办理业务的时候,第3个人来了,我们看看是如何执行的
try {
for (int i = 1; i <=3; i++) {
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "=> OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
可以看到,柜台1(thread-1)办理完业务就会给新来的第三人办理业务,这很符合我们生活中的场景对吧!也就是说如果两个柜台(2个核心线程)正在办理业务,那么第三个人先到等候区(阻塞队列)等待,当可以办业务的时候就会轮到第三个人。以此类推,等候区位置(阻塞队列的容量)有3个,都是一个业务办完,继续办下一个人的。这里就不放结果了。
有一天又产生新的问题了,两个柜台也在办业务,等候区人满了,那么第6个人来了怎么办呢?
try {
for (int i = 1; i <=6; i++) {
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "=> OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
看结果!
发现新的柜台(thread-3)启用了!这样就能解决人满为患的问题。以此类推我们会启用第四、第五个柜台!这样就没问题了吗?看下面
有一天这个银行太火爆了,柜台全部启用都在办理业务,等候区人满,这时候又来了人怎么办?
try {
for (int i = 1; i <=9; i++) {
poolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "=> OK");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
poolExecutor.shutdown();
}
这个时候就会关系到拒绝策略的问题了!我们这里用的是new ThreadPoolExecutor.AbortPolicy()
策略。这个策略会在执行不过来任务的时候抛出异常。当然如果你用我的代码来跑没有出异常,多跑几次(因为如果之前的线程迅速执行完任务,就会把后面的任务执行到,现实中也是符合逻辑的对吧,有的柜台办理的快一些)
四种拒绝策略
以下四种策略都是基于:队列满了、启用了最大线程数跑任务的时候又来了新任务
- new ThreadPoolExecutor.AbortPolicy():抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy():丢给原线程执行
- new ThreadPoolExecutor.DiscardPolicy():丢掉任务(执行的快也可能不丢)
- new ThreadPoolExecutor.DiscardOldestPolicy():和老任务竞争(执行的快也可能不丢)
把执行策略换一下执行以下就明白了,这里讲一下第二个丢给原线程策略
更换为CallerRunsPolicy策略会发现丢给原线程(生活中就是公司让你去银行办业务,银行说办不了应该在原公司就能办理,结果你回原公司把事办了)
最大线程如何定义?(调优)
上面说了那么多关于如何创建线程池以及线程池参数的选择,那么到底依据哪些条件判断该如何设置参数呢?
-
CPU密集型:几核CPU,maximumPoolSize就设置为几,保证CPU效率最高。
//获取CPU核数 System.out.println(Runtime.getRuntime().availableProcessors());
-
IO密集型:判断程序中十分消耗IO的线程,也就是内存/磁盘占用率较高的程序
假如一个程序中有15个线程大型IO操作,非常消耗资源,必须留15个线程来执行IO。那么我们如何设置最大线程数呢?建议设置两倍,保证15个线程IO正常执行时还有空余线程执行别的东西。
关于CPU密集型和IO密集型的介绍
什么是CPU密集型、IO密集型?https://www.cnblogs.com/aspirant/p/11441353.html.