走进Java线程池,解决你对Java线程池的种种疑问 (二)
Executors是一个Java中的工具类,提供工厂方法来创建不同类型的线程池。
前面我们已经介绍了关于Executors工具类中常用的创建线程池的方法,我们简单进行回顾下:
- 1.newSingleThreadExecutor
newSingleThreadExecutor |
---|
介绍:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来代替它。此线程池保证所有的任务的执行顺序按照任务的提交顺序执行。 |
优点:单线程的线程池,保证线程的顺序执行 |
缺点:不适合并发 |
- 2.newFixedThreadPool
newFixedThreadPool |
---|
介绍:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大值。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 |
优点:固定大小线程池,超出的线程会在队列中等待 |
缺点:不支持自定义拒绝策略,大小拒绝,难以扩展 |
- 3.newCachedThreadPool
newCachedThreadPool |
---|
介绍:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程。当任务增加时,此线程又可以添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者JVM)能够创建的最大线程大小。 |
优点:很灵活,具有弹性的线程池线程管理,用多少线程池给多大的线程池,不用后及时进行回收,用时可以进行新建 |
缺点:一旦线程无限增长,会导致内存溢出 |
- newScheduledThreadPool
newScheduledThreadPool |
---|
介绍:创建一个定长线程池,支持定时与周期性任务的执行 |
优点:一个固定大小的线程池,可以定时或者周期性执行任务 |
缺点:任务是单线程方式执行,一旦一个任务失败其他任务也会受到影响 |
注意:
1.以上线程池都不支持自定义拒绝策略
2.newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至会导致内存溢出
3.newCachedThreadPool和newScheduledPool:
主要问题是线程最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至内存溢出
由于Executors中提供创建线程池的方法具有以上的弊端,所以会推荐使用ThreadPoolExecutor创建线程池,集以上优点于一身。
1.对ThreadPoolExecutor的理解
Java提供了实现的线程池的模型–ThreadPoolExecutor,可以这样理解Java线程池:就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理的思想。
- 首先,我们需要关注ThreadPoolExecutor的构造方法:
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;
}
细看ThreadPoolExecutor的构造方法还是比较复杂的,下面我们探讨下构造方法中的核心参数。
1.1 ThreadPoolExecutor核心参数理解(面试常问)
参数名称 | 参数解释 |
---|---|
corePoolSize | 表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁。 |
maximumPoolSize | 表示线程池能够容纳可同时执行的最大线程数 |
keepAliveTime | 表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下corePoolSize个线程位置 |
unit | keepAliveTime的时间单位,最终都会转换为纳秒 |
workQueue | 当请求线程数大于maximumPoolSize时,线程进入该阻塞队列 |
threadFactory | 表示线程工厂,用来生产一组相同任务的线程,同时也可以通过增加前缀名,虚拟机栈分析时会更加清晰 |
handler | 执行拒绝策略,当workQueue达到上限,同时也达到maximumPoolSize就要通handler来进行处理。例如拒绝,丢弃等,这是一种限流的保护措施 |
整体任务执行如下:
试想,如果有请求就创建一个线程,请求结束就销毁一个线程,频繁往复这样操作,这样的代价是不能接受的。
可以看到,使用线程池不但完成手动创建线程可以做到的工作,同时也填补了手动线程不能做到的空白。
由此可见线程池的作用可以包含:
1.实现任务线程队列缓存策略和拒绝机制
2.利用线程池管理线程,控制最大并发数(手动创建线程很难得到保证)
3.可以实现定时执行,周期执行等功能
1.2 线程池中任务的执行过程分析
当任务提交到线程池中需要经过以下过程:
执行流程解析 |
---|
1.首先检查核心线程池是否已满。这个核心线程池,就是不管用户量多少,线程池始终维持的线程的池子。在这里可以假如说线程池的总容量装100个线程,核心线程数我们设置为50,那么无论用户量有多少,都保持50个线程存活着。核心线程池中线程数量是根据具体业务需求来决定的。 |
2.判断阻塞队列是否已满,阻塞队列有很多种,在下面也会简单介绍各种阻塞队列 |
3.最后判断线程池是否已满。按照前面的例子,就是判断线程数是不是100个线程,而不是50个。 |
4.如果满了,就不能继续创建线程了,就需要按照饱和策略或者拒绝策略来进行处理。拒绝策略在下面也会简单介绍。 |
简单来说:有请求时,创建线程执行任务,当线程数量等于corePoolSize(常驻核心线程数),请求假如阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。当任务处理不过来的时候,线程池开始执行拒绝策略。
1.3 阻塞队列的介绍
-
何为阻塞?简单的来说,食堂打饭的窗口只有一个,下课了同时来了很多同学,窗口每次只能服务于一个学生。剩下的学生只能排队等待。
-
何为队列?先进先出的一种数据结构(容器),例如:食堂排队打饭。
-
什么时候使用阻塞队列?生产者消费者模式
阻塞队列的工作流程:
- 下面是阻塞队列的介绍:
阻塞队列的名称 | 含义 |
---|---|
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列,默认大小为Integer.MAX_VALUE |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列} |
SynchronousQueue | 一个不存储元素的阻塞队列。如果生产者线程将元素放入队列,消费者线程没有从队列中拿走元素,那么该队列会进入阻塞状态 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列 |
LinkedBlockingDueue | 一个由链表结构组成的双向阻塞队列 |
1.4 线程池拒绝策略
我们很难准确的预测未来的最大并发量,所以定制合理的拒绝策略是不可忽略的步骤。ThreadPoolExecutor提供了四种拒绝策略:
- 1.AbortPolicy:默认的拒绝策略,会throw RejectedExecutionException拒绝
- 2.CallerRunsPolicy:提交任务的线程自己去执行该任务
- 3.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
- 4.DiscardPolicy:相当大胆的策略,直接丢弃任务,没有任何异常抛出
不同的框架也都有不同的拒绝策略,我们也可以通过实现RejectedExecutionHandler 自定义的拒绝策略。
没有绝对的拒绝策略,只有适合业务需求的拒绝策略,但在设计过程中千万不要忽略掉拒绝策略就可以。
总结
当我们需要频繁的创建线程时,我们要考虑到利用线程池统一管理线程资源,避免额外的开销和不可控的风险。同时了解到了线程池的几个核心参数之后,我们需要经过调优的过程来设置最佳线程参考值。希望大家看到此文对大家线程池部分的学习有所帮助。