公众号|松华说|谈谈线程池
  • 分享在京东工作的技术感悟,还有JAVA技术和业内最佳实践,大部分都是务实的、能看懂的、可复现的

扫一扫
关注公众号

谈谈线程池

博客首页文章列表 松花皮蛋me 2020-11-02 15:20

线程池的使用场景通常有两大类。

第一类场景是,并发执行,提高吞吐量。比如对多张图片进行校验,校验项有多个。

在这种场景下,很容易出现父子任务,父子任务共用一个线程池的话可能会出现死锁,这个是需要特别留心的。

另外,当使用Future接收多线程的执行结果时,不要在循环中出现结果为失败或者获取超时就中断,这样后面的Future就无处安放了,长时间运行突然并发升高可能会引起服务不可用。

还有一个注意事项是timeout超时叠加问题,这个问题已经有成熟的方法,就是AbstractExecutorService的invokeAll方法,它能让每个任务的超时时间都是相同的。

第二类场景是,线程隔离。一个请求绑定一个线程,请求链路中的所有任务都是同步进行。

在这种场景中,超时时间配置项是需要特别留心的。超时时间过长就会一直占着线程,服务逐渐地无法接收新请求,这种异常在RPC调用中很常见。如果提供者无法做到实时,那么可以使用下面这种方式,也就是接收请求后将其封装成任务,然后放入到业务线程中执行,执行完再回调,这样就不用担心服务拒绝新请求了。

在第一类场景中,可能有多个业务都需要使用线程池,那是否需要配置多个线程池呢?

按照业务的特性来配置不同的核心线程数、不同的拒绝策略,这种区别对待通常能够隔离异常。但是不能无视的一点是,服务器的CPU资源是固定的,大家都在抢占资源,显然这种做法并不能达到理想的隔离效果。

我在工作中看到比较多的线程池策略是,创建一定量不被回收的核心线程,当线程都在忙碌时,新任务将被放到队列中,当队列满了后再创建线程,创建失败就拒绝任务。核心线程数通过Runtime.getRuntime().availableProcessors() 计算得出,理想情况下是CPU核心数,但是在容器这种环境下是不准的,感兴趣的朋友可以在网上检索下:JAVA程序运行在容器有哪些影响。

这里说的,存放排队任务的队列,在并发突然变大时起着缓冲作用,但是会增大响应时长,对于实时交互场景来说是不能接受的。因此,核心线程数配置成多少,是性能压力测试重点关注的关键指标。我们一般通过重写线程池生命周期的afterExecute方法,统计分析队列积压情况,评估核心线程数设置是否恰当。

然而,已经调优过的核心线程数并不总是准确的,当依赖服务的性能变差时,新任务可能无法分配到线程,只能排队执行了,这样响应时长就会出现波动。即使通过配置中心来动态管理参数,也是不能及时止损的。

所以最好的方案是,不使用队列,尽可能创建更多的线程来处理任务,当前没有空闲线程就创建一个新的线程。甚至,还可以通过prestartAllCoreThreads方法,提前初始化核心线程进行预热。使用这种线程池策略,我们需要提前检查服务器ulimit资源限制情况,还需要避免频繁回收线程。

另外,为了避免服务器的CPU利用率、内存利用率出现过载,通常采用限流、降级来保护系统。我们不限制线程数而是限制请求数,是希望优先保证已经在服务区的客户体验,而不是客户越多越好,因为质量比数量更重要。

完,下期见!