作为运维,你不一定要会写Java代码,但是一定要懂Java在生产跑起来之后的各种机制。
本文为《Hi,运维,你懂Java吗》系列文章 第九篇,敬请关注后续系列文章
欢迎关注 龙叔运维(公众号) 持续分享运维经验
前言
本篇对java的线程池进行讲解,线程池对java应用的性能来说有很重要的影响。
1、什么是线程池
类似数据库连接池,主要是为了避免线程不断创建销魂造成的大量资源消耗。
线程池按照设定管理线程。控制运行的线程的数量,将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕, 再从队列中取出任务来执行。主要特点为:线程复用;控制最大并发数;管理线程。
【线程池的处理流程】
如上图,有几个概念,下面进行简单讲解:
a、核心线程:核心线程是在空闲时也不会被回收的线程,可以说是常驻线程,数量由corePoolSize参数控制
b、非核心线程:当核心线程都在处理任务,阻塞队列也满了的情况下,有任务进来,当前线程数没有达到maximumPoolSize参数设置的最大值的时候,就会创建新的线程处理队列中的任务,这些非核心线程不是常驻的,空闲时间超过keepAliveTime 参数设置的时间(时间单位由unit 参数设置)就会被回收
c、阻塞队列/工作队列:核心线程数满之后,新进入的任务加入到队列中等待被执行,队列也有四种,由workQueue参数设置,下面会单独讲解
d、拒绝策略:当阻塞队列满,且线程数已达到最大线程数限制,就会执行拒绝策略,拒绝策略也有四种,由handler参数设置,下面会单独讲解
2、线程池配置
创建线程池的时候一共有7个参数:
a、corePoolSize:核心线程数
b、maximumPoolSize:最大线程数
c、keepAliveTime:非核心线程的最大空闲时间(为0表示非核心线程空闲就立即回收),只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程中的线程数不大于corepoolSIze。
d、unit:时间单位,作用于keepAliveTime,
e、threadFactory:线程工厂,可以设置线程名,是否为守护线程等
f、workQueue:阻塞队列
g、handler:拒绝策略
如下为线程池创建代码:
public ExecutorService executorService(){ return new ThreadPoolExecutor( POLLER_THREAD_COUNT, POLLER_THREAD_COUNT * 8, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(64), new ThreadFactoryBuilder().setNameFormat("AppName_FutureTask-%d").setDaemon(true).build(), new ThreadPoolExecutor.CallerRunsPolicy()); } |
3、线程池状态
(1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
(3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
(5)TERMINATED:执行完terminated()钩子方法之后的状态。terminated钩子方法在Executor终止时调用,默认实现不执行任何操作
4、常见线程池
线程池 | 描述 | corePoolSize | maximumPoolSize | keepAliveTime | 阻塞队列 |
---|---|---|---|---|---|
FixedThreadPool | 创建指定数量线程,且线程一直存在,阻塞队列很大 | 创建时传参 | 和corePoolSize一样 | 0 | 无界队列 LinkedBlockingQueue |
CachedThreadPool | 没有常驻线程,根据需要增加和减少,使用无容量的同步阻塞队列 | 0 | Integer.MAX_VALUE | 60秒 |
|
ScheduledThreadPool | 该线程池用于执行定时任务和周期性任务。可以指定线程池中的线程数量。 | 创建时传参 | Integer.MAX_VALUE | 0 | 无界队列(延迟、排序) DelayedWorkQueue |
SingleThreadExecutor | 该线程池只会创建一个线程,用于执行所有的任务。 | 1 | 1 | 0 | 无界队列 LinkedBlockingQueue |
SingleThreadScheduledExecutor | 一个可以周期性执行任务的单线程线程池 | 1 | Integer.MAX_VALUE | 0 | 无界队列(延迟、排序) DelayedWorkQueue |
5、阻塞队列
常用阻塞队列有四种,应该根据业务场景选择合适的
a、ArrayBlockingQueue
一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象必须明确大小,像数组一样。
b、LinkedBlockingQueue
一个可改变大小的阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象如果没有明确大小,默认值是Integer.MAX_VALUE。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
(Integer.MAX_VALUE表示:int 数据类型的最大值,即:2147483647)
c、PriorityBlockingQueue
类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
d、SynchronousQueue
同步队列。同步队列没有任何容量,往队列中插入数据之后,不能立即返回,必须等待另一个线程将数据处理掉,才会返回,相当于将放入数据到队列的这个线程阻塞住了。
这个使用场景是比较少的,使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。
e、DelayQueue
具有延迟的功能,我们可以设定在队列中的任务延迟多久之后执行。它是无界队列,但是放入的元素必须实现Delayed接口,而Delayed接口又继承了Comparable接口,所以自然就拥有了比较和排序的能力.
6、拒绝策略
线程数已经达到最大线程数,阻塞队列也满了,就会执行拒绝策略,java有下面四个策略,当然也可以自定义策略
- AbortPolicy:直接抛出 RejectedExecutionException 异常,影响系统运行
- CallerRunsPolicy:用调用者所在线程(提交任务的线程)来执行任务
- DiscardOldestPolicy:丢弃任务队列里最早的任务,把提交的任务加入任务队列
- DiscardPolicy:直接丢弃提交的任务。【如果允许丢弃任务,这就是最好的处理方式】
7、线程池常见异常
7.1、线程池过载
线程池过载是指线程数量过多造成服务器资源使用过高,降低系统性能
1、系统性能下降
2、服务器内存过高,深圳OOM
异常分析:
1、任务对象过多:线程数达到最大线程数限制,阻塞队列使用的是无界队列,并发持续很高的情况下,线程处理不过来,队列不断新增任务,导致内存撑爆
2、线程过多,线程数无限制(最大限制为Integer.MAX_VALUE),高并发任务情况下,不断创建线程处理任务,增加服务器资源消耗,如内存,线程栈不断增多,撑爆内存
异常处理:
1、重启快速恢复(但可能很快继续异常)
2、使用核实的阻塞队列,设置核实的线程数最大限制,使用合理的拒绝策略
7.2、线程池阻塞
线程池阻塞是指当线程池中的线程数量达到最大值时,新的任务会被阻塞,导致程序性能下降
java.util.concurrent.RejectedExecutionException
异常分析:
一般触发这个报错的时候,就是线程已经达到最大线程限制,且阻塞队列已满,新的任务无法处理
异常处理:
1、使用合理的拒绝策略,如果是可以忽略的任务,使用丢弃方式是最好的
2、调整阻塞队列大小,但是要适当,避免过高并发不断创建任务到队列导致内存OOM。
3、根据业务情况,如果可以异步提交,可以创建任务后就返回,不阻塞提交任务的线程