深入了解Java线程池的原理和监控
作者:摆烂的小趴菜
一、什么是线程池
简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。
二、线程池的好处
我们知道不用线程池的话,每个线程都要通过new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题
而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成GC频繁收集和停顿
因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。
所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。
三、使用线程池能解决的问题
1、效率
创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率
例如:记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3,如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!
正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了T1+T3带来的系统开销
2、阻塞
线程并发数量过多,抢占系统资源从而导致阻塞
我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况 运用线程池能有效的控制线程最大并发数,避免以上的问题
3、对线程进行一些简单的管理
比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
四、线程池的优势
在业务场景中, 如果一个对象创建销毁开销比较大, 那么此时建议池化对象进行管理。
例如线程,jdbc连接等等, 在高并发场景中, 如果可以复用之前销毁的对象, 那么系统效率将大大提升。
另外一个好处是可以设定池化对象的上限, 例如预防创建线程数量过多导致系统崩溃的场景。
五、线程池类的主要参数
- corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。
- maximumPoolSize:最大线程池大小。
- keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。
- unit:销毁时间单位。
- workQueue:存储等待执行线程的工作队列。
- threadFactory:创建线程的工厂,一般用默认即可。
- handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。
六、线程池的工作原理
如果线程池中的线程小于corePoolSize时就会创建新线程直接执行任务。
如果线程池中的线程大于corePoolSize时就会暂时把任务存储到工作队列workQueue中等待执行。
如果工作队列workQueue也满时,当线程数小于最大线程池数maximumPoolSize时就会创建新线程来处理,而线程数大于等于最大线程池数maximumPoolSize时就会执行拒绝策略。
一般流程图如下
七、线程池大小设置
配置线程池的大小可根据以下几个维度进行分析来配置合理的线程数:
- 任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。
- 任务的执行时长。
- 任务是否有依赖——依赖其他系统资源,如数据库连接等。
1、CPU密集型任务
尽量使用较小的线程池,一般为CPU核数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
2、IO密集型任务
可以使用稍大的线程池,一般为2*CPU核数+1。 因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
3、混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
4、依赖其他资源
如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。 借鉴别人的文章 对线程池大小的估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
可以得出一个结论: 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
但是通过公式计算出来的线程池大小也只是参考,准确的线程池设置大小还是需要经过性能测试验证获得的。
八、线程池监控
通过工具类监控线程池的使用、状态等,例如通过JDK自带的Jconsole、Jvisualvm监控
1)首先我们先手动创建一个简单的死锁程序并运行
2)使用JDK自带的Jconsole进行监控,选择text进程并连接
3)点击检测死锁,选择线程并查看信息,可以定位到java代码发生死锁的位置
4)使用JAVA自带的jvisualvm进行监控,选择text进程
5)选择线程TAB,查看线程信息,如果存在死锁会出现下图的样式
6)点击线程Dump之后进入新页面拖到最底下就能看到线程死锁详细信息
另外除了监控线程死锁,还可以通过线程池提供的以下参数对线程池进行监控。
- taskCount:线程池需要执行的任务数量,包括已经执行完的、未执行的和正在执行的。
- completedTaskCount:线程池在运行过程中已完成的任务数量,completedTaskCount <= taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
- getPoolSize: 线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以线程池的线程数量只增不减。
- getActiveCount:获取活动的线程数。
九、总结
对于监控而言,不在于手段的多样性,而需要明白监控的本质,以及需要的监控项内容,找出系统瓶颈,规避风险。
到此这篇关于深入了解Java线程池的原理和监控的文章就介绍到这了,更多相关Java线程池原理和监控内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!