Java 线程池
参考: blog.csdn.net/zhipengfang…
为什么需要线程池
线程的创建它会开辟本地方法栈、虚拟机栈、程序计数器等线程私有的内存,同时销毁的时候需要销毁以上3个区域,因此频繁的创建和消耗比较消耗系统资源;
当任务量远远大于线程可以处理的任务量的时候,并不能友好拒绝任务。
基于线程的以上两个缺点,为了解决这样的缺点,我们引入了线程池。
什么是线程池
就是使用池化技术来管理线程和使用线程的方式。类似于生活中的一个仓库,当我们需要某个工具的时候直接去仓库取就可以了,用完放回去,下次还能继续使用。而线程池就是放线程的一个“仓库”,需要的时候取,用完放回去,下次还能用,不需要在频繁的创建和销毁线程了。线程池中的任务队列可以很好地管理并执行任务。线程池是长生命周期的,启动后不会主动关闭。
线程池的优点
-
可以避免频繁的创建和消耗线程。
-
可以更好的管理线程的个数和资源的个数。
-
拥有更多的功能,比如线程池可以进行定时任务的执行。
-
线程池可以更友好的拒绝不能处理的任务。
线程池工作原则
线程池遵循以下工作原则:
- 如果当前线程池中的线程数量小于核心线程数,会直接创建新的线程来执行任务。
- 如果当前线程池中的线程数量大于等于核心线程数,但任务队列未满,任务会被放入队列中等待执行。
- 如果任务队列已满,但当前线程池中的线程数量小于最大线程数,会创建新的线程来执行任务。
- 如果当前线程池中的线程数量达到最大线程数,且任务队列已满,根据指定的拒绝策略来处理无法执行的任务。
线程池的各个参数详细介绍
-
corePoolSize(核心线程数): corePoolSize表示线程池中的核心线程数量,即线程池中始终保持的活动线程数量。如果当前线程池中的线程数量小于核心线程数,会直接创建新的线程来执行任务。
-
maximumPoolSize(最大线程数): maximumPoolSize表示线程池中允许的最大线程数量。如果当前线程池中的线程数量大于等于核心线程数,但任务队列未满,任务会被放入队列中等待执行。如果任务队列已满,但当前线程池中的线程数量小于最大线程数,会创建新的线程来执行任务。
-
keepAliveTime(线程空闲时间): keepAliveTime表示线程空闲时的存活时间。如果线程空闲时间超过keepAliveTime,且线程池中的线程数量大于核心线程数,多余的线程会被销毁,以减少资源消耗。
-
unit(时间单位): unit是keepAliveTime的时间单位,可以是秒、毫秒、微秒等。通过合理设置keepAliveTime和unit,可以控制线程空闲时间的精度。
-
workQueue(任务队列): workQueue是用于存放待执行任务的队列。线程池中的线程会从任务队列中获取任务并执行。常用的任务队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
-
threadFactory(线程工厂): threadFactory用于创建新的线程对象。通过实现ThreadFactory接口,可以自定义线程的创建方式。例如,可以设置线程的名称、优先级等。
-
RejectedExecutionHandler(拒绝策略): RejectedExecutionHandler用于处理线程池无法执行的任务。常见的拒绝策略有四种:
- ThreadPoolExecutor.AbortPolicy(默认策略): 当线程池无法执行任务时,会抛出RejectedExecutionException异常。
- ThreadPoolExecutor.CallerRunsPolicy: 当线程池无法执行任务时,会将任务返回给调用者来手动处理。
- ThreadPoolExecutor.DiscardPolicy: 当线程池无法执行任务时,会直接丢弃任务,不会抛出任何异常。
- ThreadPoolExecutor.DiscardOldestPolicy: 当线程池无法执行任务时,会丢弃最旧的任务,然后尝试再次执行当前任务。
ThreadPoolExecutor是如何运行
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
Executors 的 四种线程池工具类
- FixedThreadPool: FixedThreadPool是一种固定大小的线程池,它的核心线程数和最大线程数是相等的,任务队列使用LinkedBlockingQueue。适用于负载较重且任务数量相对固定的场景,如电商商品批量上架。
- CachedThreadPool: CachedThreadPool是一种大小可变的线程池,它的核心线程数为0,最大线程数为Integer.MAX_VALUE,线程空闲时间为60秒,任务队列使用SynchronousQueue。适用于负载较轻且任务数量不确定的场景,如电商订单处理。
- SingleThreadPool: SingleThreadPool是一种只有一个线程的线程池,它的核心线程数和最大线程数都是1,任务队列使用LinkedBlockingQueue。适用于需要保证任务按顺序执行的场景,如电商订单状态更新。
- ScheduledThreadPool: ScheduledThreadPool是一种定时任务线程池,可以按照指定的时间间隔或延迟来执行任务。适用于需要定时执行任务的场景,比如定时任务调度,如电商秒杀活动定时开启。
为什么不建议用Executors?
不建议使用Executors的最重要的原因是:Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue(如下图),高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。 注:LinkedBlockingQueue是有界队列,但是不设置大小的话,就默认为Integer.MAX_VALUE,相当于无界队列了。
Executors 返回的线程池对象的弊端:
- FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
通过 ThreadPoolExecutor 来创建线程池,那核心参数设置多少合适呢?
《Java 并发编程事件》这本书里介绍的一个线程数计算公式:
Ncpu = CPU 核数
Ucpu = 目标 CPU 利用率,0 <= Ucpu <= 1
W / C = 等待时间 / 计算时间的比例
要程序跑到 CPU 的目标利用率,需要的线程数为:
Nthreads = Ncpu * Ucpu * (1 + W / C)
这公式太偏理论化了,很难实际落地下来,首先很难获取准确的等待时间和计算时间。再着一个服务中会运行着很多线程,比如 Tomcat 有自己的线程池、Dubbo 有自己的线程池、GC 也有自己的后台线程,我们引入的各种框架、中间件都有可能有自己的工作线程,这些线程都会占用 CPU 资源,所以通过此公式计算出来的误差一定很大。
所以说怎么确定线程池大小呢?
其实没有固定答案,需要通过压测不断的动态调整线程池参数,观察 CPU 利用率、系统负载、GC、内存、RT、吞吐量 等各种综合指标数据,来找到一个相对比较合理的值。
所以不要再问设置多少线程合适了,这个问题没有标准答案,需要结合业务场景,设置一系列数据指标,排除可能的干扰因素,注意链路依赖(比如连接池限制、三方接口限流),然后通过不断动态调整线程数,测试找到一个相对合适的值。
线程池咋监控
5种告警规则。
- 线程池活跃度告警。活跃度 = activeCount / maximumPoolSize,当活跃度达到配置的阈值时,会进行事前告警。
- 队列容量告警。容量使用率 = queueSize / queueCapacity,当队列容量达到配置的阈值时,会进行事前告警。
- 拒绝策略告警。当触发拒绝策略时,会进行告警。
- 任务执行超时告警。重写 ThreadPoolExecutor 的 afterExecute() 和 beforeExecute(),根据当前时间和开始时间的差值算出任务执行时长,超过配置的阈值会触发告警。
- 任务排队超时告警。重写 ThreadPoolExecutor 的 beforeExecute(),记录提交任务时时间,根据当前时间和提交时间的差值算出任务排队时长,超过配置的阈值会触发告警
线程使用需要注意的
- OOM 问题。刚开始使用线程都是通过 Executors 创建的,前面说了,这种方式创建的线程池会有发生 OOM 的风险。不要使用Executors 创建线程
- 任务执行异常丢失问题。可以通过下述4种方式解决
- 在任务代码中增加 try、catch 异常处理
- 如果使用的 Future 方式,则可通过 Future 对象的 get 方法接收抛出的异常
- 为工作线程设置 setUncaughtExceptionHandler,在 uncaughtException 方法中处理异常
- 可以重写 afterExecute(Runnable r, Throwable t) 方法,拿到异常 t
-
共享线程池问题。整个服务共享一个全局线程池,导致任务相互影响,耗时长的任务占满资源,短耗时任务得不到执行。同时父子线程间会导致死锁的发生,导致 OOM
-
跟 ThreadLocal 配合使用,导致脏数据问题。我们知道 Tomcat 利用线程池来处理收到的请求,会复用线程,如果我们代码中用到了 ThreadLocal,在请求处理完后没有去 remove,那每个请求就有可能获取到之前请求遗留的脏值。
-
ThreadLocal 在线程池场景下会失效,可以考虑用阿里开源的 Ttl 来解决
以上提到的线程池动态调参、通知告警在开源动态线程池项目 DynamicTp 中已经实现了,可以直接引入到自己项目中使用。
DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。