不同线程池的使用场景,java线程池应用实例

  不同线程池的使用场景,java线程池应用实例

  00-1010简介1、coreSize==maxSize2、maxSize Unbounded synchronous Queue 3、maxSize有界队列Unbounded 4、maxSize有界队列Bounded 5、keepAliveTime Set Infinite 6、共用和独立线程池7、如何计算线程大小和队列大小8、总结

  00-1010线程池执行器初始化时,主要有以下参数:

  public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueueRunnable workQueue,Factory ThreadThreadFactory,rejecte execution Handler Handler){这些参数大家应该都很熟悉。虽然参数很少,但实际工作中门道很多。大部分问题主要集中在线程大小和队列大小的设置上。接下来,我们来看看如何在工作中初始化ThreadPoolExecutor。

  00-1010相信很多人都看过,或者自己写过这段代码:

  ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,600000L,TimeUnit。DAYS,new LinkedBlockingQueue());这行代码主要说明在初始化ThreadPoolExecutor时,coreSize和maxSize是相等的。如果这样设置,随着请求数量的增加,将会是这样的:

  请求coreSize时添加线程;当请求数=coreSize队列不满足时,将任务加入队列;当队列满的时候,任务会因为coreSize和maxSize相等而被直接拒绝。这样写的最大目的就是一次性把线程增加到maxSize,不回收线程,防止线程回收,避免增加回收损失。一般来说,业务流量有其高峰和低谷,流量低的时候,线程不会被回收;当流量达到峰值时,maxSize线程可以应对峰值,而无需缓慢初始化到maxSize的过程。

  此设置有两个前提条件:

  AllowCoreThreadTimeOut默认为false,但不会主动设置为true。如果allowCoreThreadTimeOut为false,则线程空闲时不会回收核心线程;

  我们会把keepAliveTime和TimeUnit设置的很大,这样线程会闲置很长时间,线程不容易被回收。

  现在我们机器的资源非常丰富,不用担心空闲线程会浪费机器的资源,所以目前这种写法非常普遍。

  00-1010在线程池中选择队列时,我们也会看到有同学选择SynchronousQueue。我们在《SynchronousQueue 源码解析》章说过,它里面有两种形式:栈和队列。默认是stack的形式,里面没有存储容器。放元素和拿元素是一一对应的。例如,我使用put方法来放置元素。如果此时没有对应的take操作,那么put操作将被阻塞,直到有线程来执行take操作,put操作才会返回。

  基于这个特性,如果要使用SynchronousQueue,就需要把maxSize设置得尽可能大,这样才能接受更多的请求。

  假设我们将maxSize设置为10,并选择SynchronousQueue队列。假设所有请求都被put,而没有请求被take,前10个put请求将消耗10个线程,所有这些线程都在put操作中被阻塞。在第11个请求到来后,该请求将被拒绝。这就是为什么我们说要将maxSize设置得尽可能大,以防止请求被拒绝。

  MaxSize Unbounded synchronous queue有明显的优点和缺点:

  优势:

  当任务被消耗后,会被返回,这样请求就可以知道当前请求是否已经被消耗,如果是另一个团队。

  列的话,我们只知道任务已经被提交成功了,但无法知道当前任务是在被消费中,还是正在队列中堆积。

  缺点:

  比较消耗资源,大量请求到来时,我们会新建大量的线程来处理请求;

  如果请求的量难以预估的话,maxSize 的大小也很难设置。

  

 

  

3、maxSize 有界 + Queue 无界

在一些对实时性要求不大,但流量忽高忽低的场景下,可以使用 maxSize 有界 + Queue 无界的组合方式。

 

  比如我们设置 maxSize 为 20,Queue 选择默认构造器的 LinkedBlockingQueue,这样做的优缺点如下:

  优点:

  电脑 cpu 固定的情况下,每秒能同时工作的线程数是有限的,此时开很多的线程其实也是浪费,还不如把这些请求放到队列中去等待,这样可以减少线程之间的 CPU 的竞争;

  LinkedBlockingQueue 默认构造器构造出来的链表的最大容量是 Integer 的最大值,非常适合流量忽高忽低的场景,当流量高峰时,大量的请求被阻塞在队列中,让有限的线程可以慢慢消费。

  缺点:

  流量高峰时,大量的请求被阻塞在队列中,对于请求的实时性难以保证,所以当对请求的实时性要求较高的场景,不能使用该组合。

  

 

  

4、maxSize 有界 + Queue 有界

这种组合是对 3 缺点的补充,我们把队列从无界修改成有界,只要排队的任务在要求的时间内,能够完成任务即可。

 

  这种组合需要我们把线程和队列的大小进行配合计算,保证大多数请求都可以在要求的时间内,有响应返回。

  

 

  

5、keepAliveTime 设置无穷大

有些场景下我们不想让空闲的线程被回收,于是就把 keepAliveTime 设置成 0,实际上这种设置是错误的,当我们把 keepAliveTime 设置成 0 时,线程使用 poll 方法在队列上进行超时阻塞时,会立马返回 null,也就是空闲线程会立马被回收。

 

  所以如果我们想要空闲的线程不被回收,我们可以设置 keepAliveTime 为无穷大值,并且设置 TimeUnit 为时间的大单位,比如我们设置 keepAliveTime 为 365,TimeUnit 为 TimeUnit.DAYS,意思是线程空闲 1 年内都不会被回收。

  在实际的工作中,机器的内存一般都够大,我们合理设置 maxSize 后,即使线程空闲,我们也不希望线程被回收,我们常常也会设置 keepAliveTime 为无穷大。

  

 

  

6、线程池的公用和独立

在实际工作中,某一个业务下的所有场景,我们都不会公用一个线程池,一般有以下几个原则:

 

  查询和写入不公用线程池,互联网应用一般来说,查询量远远大于写入的量,如果查询和写入都要走线程池的话,我们一定不要公用线程池,也就是说查询走查询的线程池,写入走写入的线程池,如果公用的话,当查询量很大时,写入的请求可能会到队列中去排队,无法及时被处理;

  多个写入业务场景看情况是否需要公用线程池,原则上来说,每个业务场景都独自使用自己的线程池,绝不共用,这样在业务治理、限流、熔断方面都比较容易,一旦多个业务场景公用线程池,可能就会造成业务场景之间的互相影响,现在的机器内存都很大,每个写入业务场景独立使用自己的线程池也是比较合理的;

  多个查询业务场景是可以公用线程池的,查询的请求一般来说有几个特点:查询的场景多、rt 时间短、查询的量比较大,如果给每个查询场景都弄一个单独的线程池的话,第一个比较耗资源,第二个很难定义线程池中线程和队列的大小,比较复杂,所以多个相似的查询业务场景是可以公用线程池的。

  

 

  

7、如何算线程大小和队列大小

在实际的工作中,我们使用线程池时,需要慎重考虑线程的大小和队列的大小,主要从几个方面入手:

 

  根据业务进行考虑,初始化线程池时,我们需要考虑所有业务涉及的线程池,如果目前所有的业务同时都有很大流量,那么在对于当前业务设置线程池时,我们尽量把线程大小、队列大小都设置小,如果所有业务基本上都不会同时有流量,那么就可以稍微设置大一点;根据业务的实时性要求,如果实时性要求高的话,我们把队列设置小一点,coreSize == maxSize,并且设置 maxSize 大一点,如果实时性要求低的话,就可以把队列设置大一点。假设现在机器上某一时间段只会运行一种业务,业务的实时性要求较高,每个请求的平均 rt 是 200ms,请求超时时间是 2000ms,机器是 4 核 CPU,内存 16G,一台机器的 qps 是 100,这时候我们可以模拟一下如何设置:

  4 核 CPU,假设 CPU 能够跑满,每个请求的 rt 是 200ms,就是 200 ms 能执行 4 条请求,2000ms 内能执行 2000/200 * 4 = 40 条请求;

  200 ms 能执行 4 条请求,实际上 4 核 CPU 的性能远远高于这个,我们可以拍脑袋加 10 条,也就是说 2000ms 内预估能够执行 50 条;

  一台机器的 qps 是 100,此时我们计算一台机器 2 秒内最多处理 50 条请求,所以此时如果不进行 rt 优化的话,我们需要加至少一台机器。

  线程池可以大概这么设置:

  

ThreadPoolExecutor executor = new ThreadPoolExecutor(15, 15, 365L, TimeUnit.DAYS, new LinkedBlockingQueue(35));

线程数最大为 15,队列最大为 35,这样机器差不多可以在 2000ms 内处理最大的请求 50 条,当然根据你机器的性能和实时性要求,你可以调整线程数和队列的大小占比,只要总和小于 50 即可。

 

  以上只是很粗糙的设置,在实际的工作中,还需要根据实际情况不断的观察和调整。

  

 

  

8、总结

线程池设置非常重要,我们尽量少用 Executors 类提供的各种初始化线程池的方法,多根据业务的量,实时性要求来计算机器的预估承载能力,设置预估的线程和队列大小,并且根据实时请求不断的调整线程池的大小值。

 

  以上就是java线程池不同场景下使用示例经验总结的详细内容,更多关于java线程池不同场景使用经验的资料请关注盛行IT其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: