JAVA线程池调优

摘要:
在JAVA中,可以使用自定义代码管理线程,应用程序也可以使用线程池。在使用线程池时,一个因素至关重要:调整线程池的大小对于获得最佳性能至关重要。线程池具有最小线程数和最大线程数。假设JVM有4个可用的CPU,很明显,最大线程数应该至少设置为4个。指定最小线程数的负面影响很小。对于任务队列,线程池通常会限制其大小。必须仔细调整线程池以盲目地向池中添加新线程,这在某些情况下可能会对性能产生不利影响。

    在JAVA中,线程可以使用定制的代码来管理,应用也可以利用线程池。在使用线程池时,有一个因素非常关键:调节线程池的大小对获得最好的性能至关重要。线程池的性能会随线程池大小这一基本选择而有所不同,在某些条件下,线程池过大对性能也有很多不利的影响。

    所有线程池的工作方式本质是一样的:有一个任务队列,一定数量的线程会从该任务队列获取任务然后执行。任务的结果可以发回客户端,或保存到数据库,或保存到某个内部数据结构中,等等。但是在执行完任务后,这个线程会返回任务队列,检索另一个任务并执行。

    线程池有最小线程数和最大线程数。池中会有最小数目的线程随时待命,等待任务指派给它们。因为创建线程的成本非常高昂,这样可以提高任务提交时的整体性能。线程池的最小线程数称作核心池大小,考虑ThreadPoolExecutor最简单的情况,如果有个任务要执行,而所有的并发线程都在忙于执行另一个任务,就会启动一个新线程,直到创建的线程达到最大线程数。

    一般我们会从以下几个方面对线程池进行设置:

  • 设置最大线程数

    对于给定硬件上的给定负载,最大线程数设置为多少最好呢?这个问题回答起来并不简单:它取决于负载特性以及底层硬件。特别是,最优线程数还与每个任务阻塞的频率有关。

    假设JVM有4个CPU可用,很明显最大线程数至少要设置为4。的确,除了处理这些任务,JVM还有些线程要做其他的事,但是它们几乎从来不会占用一个完整的CPU,至于这个数值是否要大于4,则需要进行大量充分的测试。

    有以下两点需要注意:

    一旦服务器成为瓶颈,向服务器增加负载时非常有害的;

    对于CPU密集型或IO密集型的机器增加线程数实际会降低整体的吞吐量;

  • 设置最小线程数

    一旦确定了线程池的最大线程数,就该确定所需的最小线程数了。大部分情况下,开发者会直截了当的将他们设置成同一个值。

    将最小线程数设置为其他某个值(比如1),出发点是为了防止系统创建太多线程,以节省系统资源。指定一个最小线程数的负面影响相当小。如果第一次就有很多任务要执行,会有负面影响:这是线程池需要创建一个新线程。创建线程对性能不利,这也是为什么起初需要线程池的原因。

    一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该保留几分钟,以处理任何负载飙升。空闲时间应该以分钟计,而且至少在10分钟到30分钟之间,这样可以防止频繁创建线程。

  • 线程池任务大小

    等待线程池来执行的任务会被保存到某个队列或列表中;当池中有线程可以执行任务时,就从队列中拉出一个。这会导致不均衡:队列中任务的数量可能变得非常大。如果队列太大,其中的任务就必须等待很长时间,直到前面的任务执行完毕。

    对于任务队列,线程池通常会限制其大小。但是这个值应该如何调优,并没有一个通用的规则。若要确定哪个值能带来我们需要的性能,测量我们的真实应用是唯一的途径。不管是哪种情况,如果达到了队列限制,再添加任务就会失败。ThreadPoolExecutor有一个rejectedExecution方法,用于处理这种情况,默认会抛出RejectedExecutionExecption。应用服务器会向用户返回某个错误:或者是HTTP状态码500,或者是Web服务器捕获异常错误,并向用户给出合理的解释消息—其中后者是最理想的。

  • 设置ThreadPoolExecutor的大小

    线程池的一般行为是这样的:创建时准备最小数目的线程,如果来了一个任务,而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就会立即执行。否则,任务被加入到等待队列,如果队列中已经无法加入新任务,则拒接之。

    根据所选任务队列的类型,ThreadPoolExecutor会决定何时会启动一个新线程。有以下三种可能:

    • SynchronousQueue

      如果ThreadPoolExecutor搭配的是SynchronousQueue,则线程池的行为和我们预期的一样,它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则会为新任务启动一个新线程。然而这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有的线程都在忙碌,则新的任务都会被拒绝,所以如果是管理少量的任务,这是个不错的选择,对于其他的情况就不适合了。

    • 无界队列

      如果ThreadPoolExecutor搭配的是无界队列,如LinkedBlockingQueue,则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor最多仅会按照最小线程数创建线程,也就是说最大线程池大小被忽略了。如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。

    • 有界队列

      搭配了有界队列,如ArrayBlockingQueue的ThreadPoolExecutor会采用一个非常负责的算法。比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。

      如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。

      这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。

    对于上面提到的每一种选择,都能找到很多支持或反对的依据,但是在尝试获得最好的性能时,可以应用KISS原则"Keep it simple,stupid"。可以将最小线程数和最大线程数设置为相同,在保存任务方面,如果适合无界队列,则选择LinkedBlockingQueue;如果适合有界队列,则选择ArrayBlockingQueue。

 

小结:

 

  • 有时对象池也是不错的选择,线程池就是情形之一:线程初始化的成本很高,线程池使得系统上的线程数容易控制。
  • 线程池必须需仔细调优,盲目的向池中添加新线程,在某些情况下对性能会有不利的影响。
  • 使用ThreadPoolExecutor时,选择更简单选项通常会带来最好的、最能预见的性能。

     

     

 

 

 

 

免责声明:文章转载自《JAVA线程池调优》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇@Value("#{}")与@Value("${}")的区别以及用法Nginx关闭危险的HTTP请求下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

python网络/并发编程部分简单整理

软件开发架构C/S架构:Client与Server客户端与服务器端架构 .exeB/S架构:Browser与Server浏览器端与服务器端架构IP地址: IP地址是指互联网协议地址 IP地址通常用“点分十进制”表示,实际上是32位二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)port: 设备与外界通讯交流的出口...

python中多线程,多进程,多协程概念及编程上的应用

1, 多线程  线程是进程的一个实体,是CPU进行调度的最小单位,他是比进程更小能独立运行的基本单位。  线程基本不拥有系统资源,只占用一点运行中的资源(如程序计数器,一组寄存器和栈),但是它可以与同属于一个进程的其他线程共享全部的资源。  提高程序的运行速率,上下文切换快,开销比较少,但是不够稳定,容易丢失数据,形成死锁。 直接上代码: impor...

node.js

JavaScript单线程的误解 在我接触JavaScript(无论浏览器还是NodeJS)的时间里,总是遇到有朋友有多线程的需求。而在NodeJS方面,有朋友甚至直接说到,NodeJS是单线程的,无法很好的利用多核CPU。 诚然,在前端的浏览器中,由于前端的JavaScript与UI占据同一线程,执行JavaScript确实为UI响应造成了一定程度上的麻...

Spring使用@Async注解

    本文讲述@Async注解,在Spring体系中的应用。本文仅说明@Async注解的应用规则,对于原理,调用逻辑,源码分析,暂不介绍。对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程...

rabbitMq无法消费发送的q的问题

1、问题叙述:   该项目配置了10来个mq,应对新开发需求,我也加了一个mq配置,然后在本地代码当中调用,当中接受,与前面写法相似,项目上测试环境测试。发现发送了queue之后本地消费日志没有的bug。 处理方案:   1、检查我的mq的配置,检查代码中对应mq的配置,然后重试     看不出问题出在哪里,寻找方法本地测试   2、本地测试,本地项目启动...

Java读写锁

读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实...