java线程池功能_Java线程池总结

本文深入探讨了Java线程池的重要性和使用,包括线程池的优势、ThreadPoolExecutor的参数解析、线程池的执行流程及拒绝策略。合理配置线程池能有效降低资源消耗、提高响应速度和管理效率,例如设置corePoolSize、maximumPoolSize、keepAliveTime等参数。此外,文章强调了动态调整线程池参数和监控线程池的重要性,以应对不同场景和任务需求。
摘要由CSDN通过智能技术生成

一、线程池

线程池适合处理的任务:执行时间短、工作内容较为单一。

合理使用线程池带来的好处:

1)降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的开销

2)提高响应速度:当任务到达时,任务可以不用等待线程创建就能立即执行

3)提高线程的可管理性:可以统一对线程进行分配、调优和监控

4)提供更多强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池SchedulerThreadPoolExecutor,允许任务延期执行或定期执行

线程池使应用能更加充分利用CPU、内存、网络、IO等系统资源。线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间。

在线程销毁时需要回收这些系统资源。因此频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程本身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。线程池的作用包括:

1):利用线程池管理并复用线程、控制最大并发数等

2):实现任务线程队列缓存策略和拒绝机制

3):实现某些与时间相关的功能,如定时执行、周期执行

4):隔离线程环境。通过配置两个或多个线程池,将一台服务器上较慢的服务和其他服务隔离开,避免各服务线程相互影响。

ThreadPoolExecutor

UML类图:

0063583759bbe4ef5c1304f7967a3db5.png

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

ExecutorService接口增加了一些能力:

(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;

(2)提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

主要参数:

1)corePoolSize  线程池核心线程的大小

2)maximumPoolSize  线程池最大线程的大小

3)keepAliveTime  空闲线程的存活时间

4)BlockingQueue  用来暂时保存任务的工作队列

5)RejectedExecutionHandler  线程池已经关闭或者饱和(达到了最大线程数且工作队列已满),executor()方法将调用Handler

参数详细说明:

1、corePoolSize  表示常驻核心线程数,如果大于0,则即使执行完任务,线程也不会被销毁(allowCoreThreadTimeOut为false)。因此这个值的设置非常关键,设置过小会导致线程频繁地创建和销毁,设置过大会造成浪费资源

2、maximumPoolSize  表示线程池能够容纳的最大线程数。必须大于或者等于1。

3、keepAliveTime  表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,避免浪费内存和句柄资源。在默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才起作用,达到空闲时间的线程会被销毁,直到只剩下corePoolSize个线程为止。但是当ThreadPoolExecutor的allowCoreThreadTimeOut设置为true时(默认false),核心线程超时后也会被回收。(一般设置60s)

4、TimeUnit  表示时间单位,keepAliveTime的时间单位通常是TimeUnit.SECONDS

5、BlockingQueue  表示缓存队列。

6、threadFactory  表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给threadFactory增加组名前缀来实现的。在用jstack分析时,就可以知道线程任务是由哪个线程工厂产生的。

7、handler   表示执行拒绝策略的对象。当超过workQueue的缓存上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。友好的拒绝策略可以是如下

三种:

1):保存到数据库进行削峰填谷。在空闲时再取出来执行

2):转向某个提示页面

3):打印日志

线程池处理任务规则及流程说明:

首先,所有的任务都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来的执行流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。

其执行过程如下:

1、首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

2、如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

3、如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

4、如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

5、如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

6、线程池中存活的线程执行完当前任务后,会在循环中反复从BlockingQueue队列中获取任务来执行

055ee9d792b7f23aa7ea9031d0549e25.png

总结使用线程池需要注意以下几点:

1、合理设置各类参数,应根据实际业务场景来设置合理的工作线程数

2、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

3、创建线程或线程池请指定有意义的线程名称,方便出错时回溯

4、线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式来创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。

另外,线程池中的线程数量不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加上下文切换的开销,反而起到相反的作用。

合理配置线程池

要想合理地配置线程池,就必须首先分析任务特征,可以从下面几个角度:

1)任务的性质:CPU密集型任务(处理任务不需要依赖外部 I/O),IO密集型任务(计算占比少,磁盘IO,网络IO占比多)或混合型任务

2)任务的优先级:高、中、低

3)任务的执行时间:长、中、短

4)任务的依赖性:是否依赖其他系统资源,如数据库连接,接口调用等

策略:

性质不同的任务(CPU密集型、IO密集型、速度优先、吞吐量优先)可以用不同规模的线程池分开处理,这样可以更好确定线程数量。

若CPU的核心数为N:

1)CPU密集型任务应配置尽可能少的线程,如配置核心数为 N+1 个线程的线程池。

2)由于IO密集型的任务并不是一直都在执行任务,存在读写阻塞等待,因此应配置较多的线程,如核心数2*N 个,最大线程数 25*N个

3)混合型的任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行的时间相差太大,则没必要进行分解。

4)优先级不同的任务可以使用优先级队列 PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。

5)依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

6)建议使用有界队列,并可以根据需要设置大一点。基于链表的LinkedBlockingQueue比基于数组的ArrayBlockingQueue吞吐量高,因为增删多,需要内存大。

有界队列能增加系统的稳定性和预警能力,可以根据需要设置大一点,比如几千。有时会遇到任务执行缓慢的时候,为了避免任务被拒绝,可以适当将队列设置大一些。

7)大量的耗时任务执行的时候,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先。这个时候应该设置较大队列去缓冲并发任务,调整合适的核心线程数,避免过多引发 线程上下文切换频繁导致的资源消耗(会降低处理任务的速度,降低吞吐量)

参数动态化

ThreadPoolExecutor 提供了API供ThreadPoolExecutor的实例来动态设置线程池的参数。

c30db5dc2bfdcf70e46f7a7cd36603af.png

以setCorePoolSize为例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idle的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。

结合ZooKeeper分布式配置中心,利用Zookeeper watch机制,注册节点监听,节点变动自动拉取节点配置判断线程池参数是否变化,若变化,则修改线程池参数,实现即时修改,即时生效。

线程池的监控

如果系统中大量使用线程池,则有必要对线程池进行监控,方便出现问题时,根据线程池的使用状况快速定位问题。可以通过线程池提供的方法获取线程池的信息:

getPoolSize():获取当前线程池中线程的数量

getActiveCount():获取大致的活动的线程数(正在执行任务的线程)

getLargestPoolSize():获取largestPoolSize属性,线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。

getTaskCount():返回已经计划执行的任务的大致总和

getCompletedTaskCount():返回已完成执行的任务的大致总数,小于或等于 getTaskCount()

也可通过直接调用 toString()方法获取大部分信息,当前线程池中线程的数量,活动的即正在执行任务的线程数量,队列大小,已经执行完的任务等。输出结果:

java.util.concurrent.ThreadPoolExecutor@379aea30[Running, pool size = 50, active threads = 50, queued tasks = 300, completed tasks = 1987247]

创建线程池例子,拒绝策略为打印日志并由JVM重启一个线程执行被拒绝的任务:

/*** 创建一个用于发送邮件的线程池,核心线程数为1,最大线程数为5,线程空闲时间为60s,拒绝策略为打印日志并直接执行被拒绝的任务*/

private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(50), new CustomThreadFactory("redeemSendMail"), newRejectedExecutionHandler() {

@Overridepublic voidrejectedExecution(Runnable r, ThreadPoolExecutor executor) {//打印日志

LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString());//直接执行被拒绝的任务,JVM另起线程执行

r.run();

}

});

应用

不同场景下,线程池的参数设置策略不同。

场景1:快速响应请求

追求的是响应速度,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

场景2:快速处理批量任务

任务量大,不需要瞬时完成,尽可能快执行完成即可。因此关注点在如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。

所以应设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值