线程池定义
上一章我们了解了线程的定义及使用,java使用的是内核级线程,线程的创建,销毁及上下文切换是非常消耗内核资源,如果是一个简单的dome,那么我们使用线程只需要使用 new Thread创建线程使用没有问题,但是如果是生产环境,要保证我们的程序能够长期稳定运行就需要线程池,线程池其实就可以理解为多个线程的组合管理,设计理念和我们数据库的连接池一样的
我们通过一个场景来了解:一个web应用,服务器会接收请求并处理响应到客户端,要保证各个请求之间独立访问,我们就应该给每个请求开辟一个线程来进行处理。如果大量的请求进来,势必会有大量的线程创建,因为每一次响应时间都不会太长,当处理完成我们就应该销毁当前线程,这一次次创建与销毁,不光性能不好,而且很有可能造成内存溢出(OOM)等等异常。所以我们需要线程池来实现复用及管理
线程池优势
刚才我们也了解了为什么要去创建线程池,那么他创建的原因其实也就是他的优势:
1. 降低系统资源的消耗,通过对线程池中线程的重复使用,降低创建与销毁线程的消耗
2. 提高效率,当任务到达,不用等创建好了线程才运行,可以立即执行
3. 提高线程的统一管理,线程是稀缺资源,不能无限制创建,否则影响性能及稳定性,线程池能方便线程统一管理及优化
什么时候使用线程池?
1. 大量的任务处理
2.处理任务时间较短 这两种情况下使用线程池是最合适的
线程池使用
java中使用线程池是比较简单的,基本可以说是new就可以使用,但是因为线程池创建的便捷也隐藏了他的使用隐患,与一些复杂的原理
关于线程的使用,网上有说四种的,也有说五种的,众说纷纭,那么我们来看下,为啥有四种或者五种是怎么来的,首先看一张图
我们常用的创建线程池是 Executor 及 ThreadPoolExecutor两个类,当然除了这两个,也还有其他的线程池如:ScheduledThreadPoolExecutor定时任务线程池等等,自行了解,先搞清楚了前面两个,首先是ThreadPoolExecutor,因为Executor源码创建时也是在new ThreadPoolExecutor(除了定时线程池),所以我们先搞懂它
ThreadPoolExecutor
使用只需要new ThreadPoolExecutor,1.8源码中有四个构造方法(其他版本没去翻源码,不过应该是一样的),我们之间了解最多的那个就好了
我们一个个的参数去了解:
corePoolSize : 核心线程数, 线程池长期维持的线程数不会被回收,除非线程池被销毁,需要注意的是线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
maximumPoolSize: 最大线程数,核心线程数不够会创建非核心线程,但是是当workQueue队列填满时才会创建多于corePoolSize的线程,后面讲创建流程的时候会提到
keepAliveTime: 保存生存时间,非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,如果核心线程数和最大核心线程数一致即 corePoolSize = maxPoolSize 时无效,因为都是核心线程,不能回收
unit: keepAliveTime的时间单位
workQueue: 保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
threadFactory: 新线程的创建方式,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
handler: 拒绝策略,队列已满且线程数达到maximunPoolSize,策略有四种,后面再说
这个就是我们说所的线程池创建方法,只是这些参数怎么去填才是我们需要关注的点,大家也可以尝试用自己new ThreadPoolExecutor来使用调试,看看线程创建及任务处理完成后的线程情况,当然也可以尝试其他几个构造方法的使用。
Executor
接下来看Executor的线程池创建 我们看打开其源码,我们主要先看这些个方法(其实应该是每个都有两个,都有不同的重载,代码没贴完,可以自行去查询源码,下面也会详细说明参数含义)
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory var0) {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0));
}
public static ExecutorService newCachedThreadPool(ThreadFactory var0) {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), var0);
}
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
public static ExecutorService newWorkStealingPool(int var0) {
return new ForkJoinPool(var0, ForkJoinPool.defaultForkJoinWorkerThreadFactory, (UncaughtExceptionHandler)null, true);
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory var0) {
return new Executors.DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1, var0));
}
我们一个个的去看他们的区别:
首先,他们都是创建线程池的方法,也都有两种重载
1. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,
var0 代表核心线程长度,及最大线程数,
还有一个重载是传入线程创建方式:ThreadFactory var1
2. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
一个无参,一个传入线程创建方式(对照一下源码及ThreadPoolExecutor参数)
3. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
无核心参数,最大线程数int的最大值。。。自行对照
4. newScheduledThreadPool 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
一个核心线程数,一个线程创建工厂,其他对应ThreadPoolExecutor
5. newSingleThreadScheduledExecutor 我想应该不用介绍了吧,看源码传入了一个1,也就是单线程化的定时线程池
6. newWorkStealingPool 创建一个抢占式执行的线程池(任务执行顺序不确定)1.8新加入而且源码中创建的方式也是使用的ForkJoinPool 和其他的几种区别参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面5种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。ForkJoinPool 注释还说明这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。
那现在回头看,我们已经了解了七种创建线程池的方式了,为啥有些说只有四种或者五种呢?我想可能是前面有人觉得Executor 常用的就那四种(前面四种不算newSingleThreadScheduledExecutor ),所以说线程池创建方式是四种,说五种的应该是加上了ThreadPoolExecutor。那么我们自己来分辨一下,如果面试到底应该是几种?
只要了解了源码,说几种都是对的,看你怎么划分:
首先核心的创建方式应该是只有两种
ThreadPoolExecutor ,Executor
如果按所有创建方式算,应该是七种,Executor加 ThreadPoolExecutor
注意1.8加入的newWorkStealingPool
所以我认为他们说四种,五种的都是错误的,要么两种,要么七种,要么就十三种,因为Executor每一个都有两个方法,参数不一样而已(是不是又可以装逼了,手动滑稽。。。)
那么这么多线程池创建方式,如何选择?
1. 执行很多短期异步的小程序或者负载较轻的服务器 newCachedThreadPool
2. 执行长期的任务,性能稳定 newFixedThreadPool
3. 一个任务一个任务执行 newSingleThreadScheduledExecutor ,newSingleThreadExecutor
说明一下,这两个单线程的为啥还要用线程池(有时候能遇到面试问)?一个个任务执行情况,不需要太多线程,浪费空间,因为你始终一个个执行的有顺序,另一个方面如果使用Thread还是要创建销毁,线程池虽然也只有一个线程,但是能复用,不用重新创建
4. 周期性执行任务的场景 newScheduledThreadPool
5. 充分利用线程任务量不均衡 newWorkStealingPool 对于抢占式线程推荐:JDK实现的线程池之五: ForkJoinPool、newWorkStealingPool - duanxz - 博客园 ,这一篇文章很详细的说明,及与自定义创建的对比
ExecutorService常用方法说明
1,execute(Runnablecommand):履行Ruannable类型的任务
2,submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
3,shutdown():在完成已提交的任务后封闭办事,不再接管新任务
4,shutdownNow():停止所有正在履行的任务并封闭办事。
5,isTerminated():测试是否所有任务都履行完毕了。
6,isShutdown():测试是否该ExecutorService已被关闭。
线程池的执行流程
只对ThreadPoolExecutor进行说明,下面我们看个图
1. 线程池创建好了,里面并没有线程,第一个任务来,开始创建线程执行
2. 线程数小于核心线程数,添加线程继续执行(核心线程未达到最大时,前面线程即使工作完成,也还是继续创建核心线程执行)
3.核心线程数满了,放入队列
4.队列满了,判断是否线程数是否大于最大线程数,如果不大于继续加非核心线程,如果大于触发拒绝策略
线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
源码查看:
这个就是创建线程的流程,上面是描述及翻译,也对应我给出的执行流程
线程池如何关闭?
shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true
关于线程池的相关知识就到此结束,基本能满足使用及面试等等要求,当然如果有兴趣,可以打开源码继续深入一些细节,本文就不再往下深究了,我们在工作中使用线程池要根据上面的创建方式进行选择,阿里规范给出的是不允许使用Executors,当然我个人感觉还是应该根据实际情况选择,使用 ThreadPoolExecutor确实是更好的选择,千万慎用无界线程池(int最大值)
工作中使用线程池:
最后附一个在spring boot 中简单使用线程池的源码:
/**
* 线程池定义
*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("myThreadPoolExecutor")
public Executor threadPoolExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(1);//核心线程数
taskExecutor.setMaxPoolSize(10);//最大线程数
taskExecutor.setQueueCapacity(100);//队列大小
taskExecutor.setKeepAliveSeconds(60);//保持存活时长
taskExecutor.setThreadNamePrefix("threadPoolExecutor-");//名称前缀
//下面两个是线程关闭需要注意的问题
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);//线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean,默认false
taskExecutor.setAwaitTerminationSeconds(60);//设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
return taskExecutor;
}
}
@Component //方便调用,实际使用之间使用下面注解就是一个任务
public class TestThreadPool{
@Async("myThreadPoolExecutor")
public void testTask(){
System.out.println("第一个线程池任务");
}
@Async("myThreadPoolExecutor")
public void testTask1(){
System.out.println("第二个线程池任务");
}
}