文章目录
- 一、什么是线程池?
- 1.线程池介绍
- 2.线程池使用场景
- 3.线程池的好处
- 二、线程池运行流程
- 1.构造函数的7个核心参数
- 2.线程池工作流程
- 3.线程池创建方式
- 正确的创建线程池的方法
- 4.线程池状态转换
- 5.线程池阻塞队列
- 线程池为什么使用阻塞队列?
- 6.线程池拒绝策略
- 拒绝时机
- 4种拒绝策略
- 7.线程池钩子方法
- 三、线程池监控
- 线程池监控参数
一、什么是线程池?
1.线程池介绍
线程池,thread pool,是一种线程使用模式,线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
2.线程池使用场景
线程池解决的核心问题就是资源管理问题。
场景1:快速响应用户请求 将调用封装成任务并行的执行; 不设置队列,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务;
场景2:快速处理批量任务 任务量大,不需要瞬时完成,而是关注如何使用有限的资源; 设置队列去缓冲并发任务,合适的corePoolSize;
场景3:异步调用
3.线程池的好处
1. 复用线程,避免反复创建和销毁线程带来的开销问题,减少JVM回收垃圾的压力
2. 加快响应速度
3. 合理利用CPU和内存
4. 统一管理线程
二、线程池运行流程
1.构造函数的7个核心参数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数 |
maxPoolSize | int | 最大线程数 |
keepAliveTime | long | 保持存活时间 |
unit | TimeUnit | 存活时间的单位 |
workQueue | BlockingQueue | 任务存储队列 |
threadFactory | ThreadFactory | 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程 |
Handler | RejectedExecutionHandler | 由于线程池无法接受你所提交的任务的拒绝策略 |
corePoolSize:指的是核心线程数,线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务
maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限
keepAliveTime:如果线程池当前的线程数多于 corePoolSize,那么如果多余的线程空闲时间超过 keepAliveTime,它们就会被终止
workQueue:工作队列 有3种最常见的队列类型∶
- 直接交接:SynchronousQueue,需要将 maximumPoolSize 设置的大一些,因为这种队列本身不会进行任务缓存。
- 无界队列:LinkedBlockingQueue,无需设置 maximumPoolSize,因为这种队列是一种无界队列,永远不会满,如果处理任务速度跟不上添加队列的速度,会导致队列中任务挤压严重。
- 有界的队列:ArrayBlockingQueue,按需设置即可,maximumPoolSize 大于 corePoolSize。
ThreadFactory:用来创建线程新的线程是由 ThreadFactory 创建的,默认使用 Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的 NORM_PRIORITY 优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。通常我们用默认的ThreadFactory就可以了
2.线程池工作流程
当调用execute方法添加任务时,线程池会判断当前运行线程是否小于corepoolsize,如果小于创建线程执行;
如果不小于则将当前任务添加到队列中;
如果队列满了线程数小于maximumpoolsize,创建非核心线程执行任务;
如果队列满了非核心线程满了启动拒绝策略;
一个线程完成任务时,它会从队列中取下一个任务来执行。
线程空闲超过keepalivetime并且非核心线程会被回收。
- 是否需要增加线程的判断顺序是:
1.corePoolSize
2.workQueue
3.maxPoolSize
3.线程池创建方式
线程池应该手动创建还是自动创建?
手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。
先来看看自动创建线程池, JDK为我们封装好了四种线程池使用方式:CachedThreadPool, FixedThreadPool, ScheduledThreadPool, SingleThreadExecutor。
-
FixedThreadPool
固定线程数量的线程池;由于传进去的 LinkedBlockingQueue 是没有容量上限的,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。 -
SingleThreadExecutor
单线程的线程池:它只会用唯一的工作线程来执行任务。
它的原理和 FixedThreadPool 是一样的,但是此时的线程数量被设置为了1。同样会产生OOM。 -
CachedThreadPool
可缓存线程池
特点:无界线程池,具有自动回收多余线程的功能
缺点:这里的弊端在于第二个参数 maximumPoolSize 被设置成 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM。 -
ScheduledThreadPool
支持定时及周期性任务执行的线程池
正确的创建线程池的方法
- 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大。
- 线程池里的线程数量设定为多少比较合适?
CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2 倍左右。
耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于 cpu 核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,推荐的计算方法:线程数=CPU核心数(1+平均等待时间/平均工作时间)
4.线程池状态转换
线程可以有如下5种状态:New 、Runnable 、Running 、Blocked 、Dead。
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //线程池中的任务数
- Running
状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0! - ShutDown
状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。 - Stop
状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。 - Tidying
状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 - Terminated
状态说明:线程池彻底终止,就变成TERMINATED状态。
状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
5.线程池阻塞队列
- FixedThreadPool 和SingleThreadExecutor 的 Queue 是LinkedBlockingQueue,基于链表 无界。
- CachedThreadPool 使用的 Queue 是 SynchronousQueue,只有一个元素 同步。
- ScheduledThreadPool来说,它使用的是延迟队列 DelayedWorkQueue。
- workStealingPool 是JDK1.8加入的
这个线程池和之前的都有很大不同。 子任务:当前线程中嵌套其他任务。 窃取:如果有线程处于空闲状态,会从其他线程的子任务中窃取线程来执行,但是不保证执行的顺序 - ArrayBlockingQueue: 基于数组 有界。
线程池为什么使用阻塞队列?
队列满阻塞避免资源浪费
- 阻塞队列原理
ArrayBlockingQueue原理: get,put操作加锁; 执行完成notiyAll。
put: 先获取可中断锁,判断数组长度与当前元素个数,如果相等notFull.await等待,insert(e)插入元素(插入完成notEmpty.signal唤醒正在等待的线程),解锁。
take: 同put类似
6.线程池拒绝策略
拒绝时机
1)当 Executor 关闭时,提交新任务会被拒绝。 2)当 Executor 对最大线程和工作队列容量使用有限边界并且已经饱和时。4种拒绝策略
AbortPolicy:直接抛出异常。 DiscardPolicy:丢弃一些队列中的任务。 DiscardOldestPolicy:丢弃最老未执行的任务。 CallerRunsPolicy:反馈到提交任务线程,由提交任务线程去执行。7.线程池钩子方法
java.util.concurrent.ThreadPoolExecutor#beforeExecute
java.util.concurrent.ThreadPoolExecutor#afterExecute
java.util.concurrent.ThreadPoolExecutor#terminated
三、线程池监控
线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、缓存线程池创建的线程过多导致内存溢出、系统假死等问题。因此,我们需要一种简单的监控方案来监控线程池的使用情况,比如完成任务数量、未完成任务数量、线程大小等信息。
线程池监控参数
线程池提供了以下几个方法可以监控线程池的使用情况:
方法 | 含义 |
---|---|
getActiveCount() | 线程池中正在执行任务的线程数量 |
getCompletedTaskCount() | 线程池已完成的任务数量,该值小于等于taskCount |
getCorePoolSize() | 线程池的核心线程数量 |
getLargestPoolSize() | 线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize |
getMaximumPoolSize() | 线程池的最大线程数量 |
getPoolSize() | 线程池当前的线程数量 |
getTaskCount() | 线程池已经执行的和未执行的任务总数 |
通过这些方法,可以对线程池进行监控,在 ThreadPoolExecutor 类中提供了几个空方法,如 beforeExecute 方法, afterExecute 方法和 terminated 方法,可以扩展这些方法在执行前或执行后增加一些新的操作,例如统计线程池的执行任务的时间等,可以继承自 ThreadPoolExecutor 来进行扩展。
方法 | 操作 |
---|---|
shutdown() | 线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量 |
shutdownNow() | 线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量 |
beforeExecute(Thread t, Runnable r) | 任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值 |
afterExecute(Runnable r, Throwable t) | 任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息 |