在程序中,我们将使用各种池技术来缓存来创建昂贵的对象,例如线程池,连接池和内存池。从基础上,某些对象是预先创建的,并将它们放入池中。使用时,将其直接删除。如果使用它,则可以重复使用它。您还将通过某种策略来调整缓存对象中的对象数量,以实现池的动态扩展。
由于线程的创建相对较贵,因此随机创建大量线程以引起性能问题。因此,短-Flat任务通常考虑使用线程池来处理而不是直接创建线程。但是,当使用线程池时,您应该注意使用线程池的使用。如果未正确使用,它将导致生产事故。
Java中的执行人类定义了一些快速的工具方法,以帮助我们快速创建线程池。“ Alibaba Java开发手册”提到,禁止这些方法创建线程池,但是应该手动手动使用新的threadpoolexecutor来创建线程池。规则是大量的血液产生事故。最典型的是newFixedThreadPool和newcachedthreadpool,这可能会由于资源耗尽而导致OOM问题。
首先,让我们看一下为什么NewFixedPool可能具有OOM。我们编写一个测试代码来初始化单个线程固定threadPool。该周期提交到线程池1亿次。每个任务都会创建一个相对较大的字符串并入睡一个小时:
执行程序后不久,日志中出现以下OOM:
不难发现NewFixedThreadPool方法的源代码并不难找到。线程池的工作队列直接具有linkedblockingqueue,默认结构方法LinkedBlockingQueue是Integer.max_value长度的队列。它可以被认为是无限的:
尽管使用newFixedThreadPool可以将工作线程控制为固定的数字,但任务队列是无限的。如果任务有很多,执行缓慢,则队列可能会迅速累积,并且内存将导致OOM。
我们现在更改了示例,然后更改为newcachedthreadpool方法以获取线程池。程序正在运行后,我还看到了以下OOM异常:
从日志可以看出,这次的原因不是创建线程。查看newcachedhreadpool的源代码。Synchronousqueue是一个没有存储空间的阻塞队列。这意味着只要有请求,就必须找到处理它的工作线程。如果没有空闲线程,您将创建另一个新线程。
由于我们的任务需要1个小时才能完成,因此进入后会创建大量线程。我们知道线程需要将某些内存空间分配为线程堆栈,例如1MB,因此无限的创建线程将不可避免地引导到OOM:
实际上,大多数Java开发人员都知道这两个线程池的特征,但它们很幸运。他们认为他们只使用线程池来执行一些轻巧的任务,这不会导致队列积累或打开大量线程。
但是现实通常是残酷的。我之前遇到过这样的事故:在用户注册后,我们致电外部服务以发送短信。当发送SMS接口正常时,它可以在100毫秒内响应。在某个时间点上,在某个时间点上,外部SMS服务不可用。我们将这项服务称为很长时间。例如,1分钟,6,000个用户可能会在1分钟内出现,生成6,000个SMS任务,需要6,000个线程。这是多长时间,因为无法创建导致OOM的线程,并且整个应用程序崩溃了。
因此,我也不建议使用执行者提供的两个快速线程池。原因如下:
我们需要根据我们自己的场景和并发条件评估线程池的几个核心参数,包括核心线程数,线程的最大数量,线程回收策略,工作队列的类型以及拒绝策略,以确保确保该策略线程池的工作行为满足需求。总体上,边界队列和可控线的数量需要设置。
在任何时候,它都应为自定义线程池指定一个有意义的名称,以促进调查。线程数量飙升时,线程锁,大量CPU的线程占用以及异常线程执行,我们经常抓住线程堆栈。这次,有意义的线名称可以促进我们的定位。
除了建议手动声明线程池外,我还建议使用一些监视方法来观察线程池的状态。线程池的组成部分通常会表现出艰苦的工作和晦涩。除非有拒绝策略,否则压力不会引发异常。如果我们可以提前观察到线程池队列的积压,或者通常可以尽早找到线程数的快速扩展。
在以前的演示中,我们使用PrintStats方法来实现最简单的监视,并输出每秒一次线程池的基本内部信息,包括线程数,活动线程的数量,活动线程,完成了多少个任务以及多少个积压任务在队列中是否存在。方程信息:
接下来,让我们使用此方法观察线程池的基本特征。
首先,自定义一个线程池。此线程池有2个核心线程,5个最大线程和带有10个最大线程的arrayblockingqueue阻塞队列,可容纳10个作为工作队列。使用默认的atbortpolicy拒绝该策略,也就是说,任务将添加到线程池未能投掷FequeeeXecutionException。此外,我们使用JODD库的ThreadFactoryBuilder方法来构造线程工厂来实现线程的自定义命名泳池线。
60秒后,该页面输出17,而3个提交失败。
日志中有三个类似的错误消息:
我们将图纸由打印播种方法打印到图表中以获取以下曲线:
在这一点上,我们可以总结出口池的默认工作行为:
我不知道您是否考虑过:Java线程池是可以首先使用工作线处理的任务,然后在Full后展开线程池。当我们的工作队列设置很大时,最大线程数似乎毫无意义,因为队列很困难,或者在扩展线程池时,没有办法使线程池更多地比线程池更多。radio稍微稍加优先打开更多线程,并将队列视为储备计划?例如,我们的示例,执行非常缓慢,需要10秒钟。如果可以将线程池扩展到5个最大线程,则可以在最后完成这些任务,并且由于线程池的较晚扩展,慢速任务为时已晚。
在这里,我只会给您一个一般的想法:
接下来,请尝试查看如何实现这样的“弹性”线程池。TOMCAT线程池也可以实现相似的效果供您学习。
不久前,我遇到了这样的事故:不时有太多警报提示,超过2,000,超过2,000。收到警报后,检查监视器,发现瞬时线程数量很大,但是一段时间后螺纹的数量将被删除。
为了找到问题,当线程数相对较高时,将捕获线程堆栈。捕获后,我们发现内存中有超过1,000个自定义线程池。从总体上讲,必须重复使用线程池,并且可以将5个线程池视为正常,并且1,000多个线程池必须异常。
在项目代码中,我们找不到声明线程池的地方。搜索执行关键字后,它已找到。事实证明,业务代码称为类库来获取线程池。类似于以下业务代码:调用ThreadPoolHelper的GetThreadPool方法以获取线程池,然后将几个任务提交到线程池处理中,看不到异常。
但是,实现ThreadPoolHelper的实现是令人惊讶的。GetThreadPool方法实际上每次都使用opecutor.newcachedthreadpool来创建线程池。
我们可以认为NewCachedThreadPool将在需要时创建必要的线程。业务代码的业务操作将向线程池提交多个慢速任务,以便通过业务操作打开多个线程。如果业务操作很大,则可以立即打开数千个线程。
那么,为什么我们可以看到监视中的线程数减少而不会在内存中破裂呢?
回到newcachedhreadpool的定义,它会发现其核心线程数为0,并且keepalivetime为60秒,也就是说,所有线程都可以在60秒后回收。。
修复此错误也很简单。使用静态字段存储线程池的引用,然后直接返回线程池的代码以返回此静态字段。在这里,我们必须记住我们的最佳实践并手动创建一个线程池。固定的ThreadPoolHelper类如下:
线程池的重要性在于重用,因此这是否意味着程序应始终使用线程池?根据任务的“优先级紧迫”来指定线程池的核心参数,包括线程号,回收策略和任务队列:对于缓慢和小的IO任务,您可以考虑更多的线程而无需太多。CPU核或原子核(原因是该线程必须安排到某个CPU进行执行。如果任务本身是CPU绑定任务,那么任务本身本身,而太多的线程只会增加线程的开销,并且它确实会增加。不会增加吞吐量),但可能需要更长的队列才能进行缓冲。
业务代码使用内存中某些数据的线程池异步处理,但是发现处理过程不涉及通过监视的内存中的IO操作涉及的处理过程。它特别高且有些不可思议。
已经发现,业务代码中使用的线程池也是由背景文件批处理处理任务使用的。也许足以变得良好。该线程池只有两个核心线程,最大线程也为2。使用100个容量为100的100个arrayblockingqueququequeququeququeququeququeququeue用作工作队列。
这是文件批处理处理的代码。程序启动后,它可以通过线程启用死周期逻辑,并将任务连续提交到线程池。该任务的逻辑是将大量数据写入文件:
可以想象,该线程池中的两个线程任务很重。通过PrintStats方法打印的日志,我们观察到下部线池的负担:
可以看出,线程池的两个线程始终处于活动状态,并且队列基本上已满。由于Callerrunspolicy拒绝处理策略,当线程填满时,任务将由任务的线程或任务执行。使用callerrunspolicy策略,可以使用callerrunspolicy策略,因此异步任务可能会成为同步执行。这也可以从日志的第四行中看到,这就是为什么这种拒绝策略是特殊的原因。
我不知道为什么编写代码设置此策略的学生可能是在测试期间,发现线程池由于任务处理而不能异常,并且不希望线程池丢弃任务,因此我终于选择了这种拒绝策略。无论如何,这些日志足以证明线程池已饱和。
可以想象,命运必须通过重复使用此类线程池作为业务代码而痛苦。我们编写代码测试并将简单任务提交到线程池中。此任务只是休眠的10毫秒,没有其他逻辑:
我们使用WRK工具在此接口上执行简单的压力测试。您可以看到TPS是75,而且性能真的很差。
如果您考虑一下,问题并不那么简单。由于执行IO任务的原始线程池使用Callerrunspolicy策略,如果线程池用于直接执行异步计算,当线程池饱和时,计算任务将通过执行Web请求的Tomcat线程执行。目前,它将进一步影响其他同步处理线程,甚至整个应用程序崩溃。
解决方案非常简单。您可以使用独立的线程池执行此类“计算任务”。计算任务具有双引号,因为我们的仿真代码执行休眠操作,并且不属于CPU绑定的操作。它与IO绑定操作更相似。
在使用单独的线程池在测试性能之前对代码进行翻新后,TPS提高到1727:
可以看出,盲目重复使用线程池混合线程的问题是,其他人定义的线程池属性可能不适合您的任务,而混合会互相干扰。例如,我们经常使用虚拟化技术来实现资源,而不是让所有应用程序直接使用物理机器。
混合线池的问题,我想与您一起添加一个坑:Java 8的平行流功能,这使我们可以轻松处理并行收集中的元素。其背后是共享相同的forkjoinpool.-1。对于CPU绑定的任务,使用此配置更合适,但是如果集合操作涉及同步IO操作(例如数据库操作,外部服务呼叫等),建议定制一个forkjoinpool(或普通线程池)。
线程池管理线程,线程是有价值的资源。应用程序的许多应用程序来自线程池的配置和使用。在今天的研究中,我通过与线程池相关的三个相关生产事故分享了几种最佳实践。
最后,我想强调的是,线程池通常缺乏作为应用程序中的核心组件的监视(如果您使用MQ中间件,例如RabbitMQ,操作和维护学生通常会帮助我们进行中间件监控),通常是在程序Crashi发现的情况下发现线程池的问题非常被动。在设计文章中,我们将重新谈论此问题及其解决方案。
原始:https://juejin.cn/post/7100002468452368392