线程作为一种稀缺资源,在线程较多的情况下,频繁创建、销毁消除将会带来较大的系统开销。线程池基于池化思想,内部维护多个线程,可以对内部的线程进行统一的管理和监控,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
线程池
1、线程池的优点
上面我们提到,线程池是一种稀缺资源,频繁创建销毁线程会造成较大的系统开销,因此引入了线程池技术。
线程池技术主要具有以下优点:
降低资源消耗。通过重复利用已经创建的线程,降低线程创建和销毁造成的损耗。
提高响应速度。当任务下达时,任务不需要等待线程创建就能直接执行。
提高线程可管理性。线程是稀缺资源,如果无限创建不仅会消耗系统资源,还会降低系统稳定性。使用线程池可以统一分配、调优和监控。
2、线程池架构设计
线程池这么好,那么需要我们自己实现吗?可以但没有必要,因为Java中以及封装好了线程池的实现。
Java中,线程池是通过Executor框架来实现的,该框架主要用到:Executor、ExecutorService、ThreadPoolExecutor、Executors这几个类。
- Executor 接口:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。
- ExecutorService 接口:扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;提供了管控线程池的方法,比如停止线程池的运行。
- AbstractExecutorService 抽象类:将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
- ThreadPoolExecutor 类:实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
3、 线程池的简单使用
Java中的Executors线程池工具类为我们提供了3种创建线程的方法:
1 | //执行一个长期的任务,性能好很多 |
尽管Executors为我们提供了3种创建线程池的方式,但是这几种创建方式并不适合在生产环境中使用:
阿里巴巴开发手册
要分析其存在的隐患,要先去了解Java中线程池的核心类:ThreadPoolExecutor
4、 ThreadPoolExecutor
上面提到道,Executors方式创建的线程池在实践中容易导致OOM,为了解释清楚,我们先分析一下ThreadPoolExecutor。
1 | public ThreadPoolExecutor(int corePoolSize, |
上面展示了ThreadPoolExecutor的构造方法,接下来我们了解这个7大参数
corePoolSize:线程池常驻核心线程数
maximumPoolSize:线程池能够容纳同时执行的最大线程数
keepAliveTime:多余的空闲线程存活时间、当空闲时间达到keepAliveTime,多余线程会被销毁直到剩下corePoolSize
unit:keepAliveTime单位
workQueue:任务队列,提交但未执行的任务。
threadFactory:线程池中工作线程的线程工厂,一般采用默认即可
handler:拒绝策略。当线程队列满并且工作线程大于线程池最大线程数,handler将指定任何拒绝新来的线程。
在使用ThreadPoolExecutor前,我们先通过图解方式讲解线程池底层执行的原理:
- 创建线程池后,等待提交过来的线程任务
- 在调用execut()方法添加一个任务请求,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,则马上创建线程运行这个任务。
- 如果正在运行的线程数量大于corePoolSize,则在workQueue中等待。
- 如果workQueue队列已满,且正在运行的线程数量还小于maximumPoolSize,则创建非核心线程立即执行这个任务。
- 如果workQueue队列已满,且正在运行的线程数量大于或等于maximumPoolSize,则会使用拒绝策略执行拒绝。
- 当一个线程完成任务,会从workQueue中取出一个人任务执行。
- 当一个线程空闲时间超过keepAliveTime,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么线程会被停用
5、拒绝策略
上面提到当等待队列已满且线程池最大线程数也满的时候,无法继续为新任务提供服务,这时候,需要拒绝策略机制合理处理这个问题。
JDK中为我们提供了4种拒绝策略:
- AbortPolicy:默认策略,直接抛出异常阻止系统正常运行。
- CallerRunPolicy:“调用者运行”机制,不会抛弃任务,也不会抛出异常,而是给调用线程池的线程执行。
- DiscardOldestPolicy:抛弃队列中等待最久的。
- DiscardPolicy:直接丢弃任务,不做任务响应。(如果允许任务丢失,将会是最好的拒绝策略)。
6、Executors风险分析
上面我们已经简单介绍了ThreadPoolExecuto构造函数的各个参数,下面我们观察Executors中几种常见线程池的方式,进而分析其风险。
1 | // FixedThreadPool |
上面代码展示了Executors中创建创建线程池的逻辑,可以看到底层都是通过 ThreadPoolExecutor 来创建,这就是为什么要在分析前先了解一下它。
从上面提到的ThreadPoolExecutor 我看可以知道
- 对于FixedThreadPool 与 SingleThreadExecutor 都是使用 LinkedBlockingQueue 作为阻塞队列,这是一个无界第队列,当阻塞的任务过多的时候,将会导致内存占用太大,从而导致OOM。
- 对于CacheThreadPool ,最大线程数没有进行限制,当线程提交过多,将会导致线程池创建的线程太多,从而导致OOM。
7、自定义线程池
从上面分析,我们知道Executors的方式创建线程池确实存在安全隐患,因此我们应该使用 ThreadPoolExecutor 来创建线程池,那么应该如何配置其参数呢?
这需要我们区分不同类型的业务场景:CPU密集型和IO密集型。
CPU密集型
CPU密集型指的是,任务需要大量的运算而没有阻塞(如IO阻塞),CPU一直在全力运算。
CPU密集型任务只有在正在的多核CPU上才能得到加锁。
可以通过Runtime.getRuntime().availableProcessors()拿到CPU核心数。
CPU密集型任务配置尽可能少的线程数量:线程数=CPU核数+1
IO密集型
由于IO密集型,线程常常是在等待IO操作而阻塞,因此应该配置尽量多的线程。
线程数量=CPU核心数*2