线程池概念和用法

什么是线程池?

通俗理解就是一个容器,里面放了一些线程,需要用时就取出来用,用完了就放回去等待下一次用。

线程池内部维护一个任务队列,从池里取出线程去执行队列里的任务。

为什么要使用线程池?

1.可以将任务的提交和执行策略解耦,便于统一管理任务执行策略,好维护,比如延时执行,设置等待时间,超时自动失败等。

2.提高性能,用已创建的线程执行任务,减少创建和销毁线程的开销。

3.约束最大线程并发数,防止无止境创建线程造成性能变差以及程序死掉。

4.活跃线程数、最大线程数等参数可配置,方便进行性能调优。

如何创建线程池?

下图例子创建了一个核心线程数为10、最大线程数为30、非核心线程闲置超时时长为3毫秒的线程池,往线程池提交了三个任务。

1.png

Java线程池ThreadPoolExecutor实现了Executor接口,提供如下几个构造函数:

2.png

ThreadPoolExecutor构造函数各个参数的含义:

1.核心线程:int corePoolSize => 该线程池中核心线程数最大值

线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程

核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉

正常情况下你不干活我也养你,因为我总有用到你的时候,但有时候特殊情况(比如我自己都养不起了),那你不干活我就要把你干掉了

2.线程池最大线程数: int maximumPoolSize 该线程池中线程总数最大值。线程总数 = 核心线程数 + 非核心线程数。

  1. 非核心线程闲置超时时长:****long keepAliveTime

该线程池中非核心线程闲置超时时长

一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

4.****时间单位:unit

指定keepAliveTime的时间单位,使用TimeUnit取值。

3.png

5.****工作队列:workQueue

该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

常用的workQueue类型:

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
  4. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

ThreadPoolExecutor的策略

当一个任务被添加进线程池时:

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  2. 线程数量达到了corePools,则将任务移入队列等待
  3. 队列已满,新建线程(非核心线程)执行任务
  4. 队列已满,总线程数又达到了maximumPoolSize,就会由 RejectedExecutionHandler抛出异常

上面的构造参数太多太复杂,怕玩不转?

Concurrent包还提供了一套基于ThreadPoolExecutor的封装,使得使用者无需关注那么多配置细节,下图是Executors创建线程池的用法:

4.png

Executors创建的四种线程池:

1.可缓存线程池: CachedThreadPool

a.没有最大线程数限制,想创建多少就创建多少

b.有空闲线程就用空闲线程,没有就创建新线程

c.复用空闲线程能减少一部分重复创建销毁的开销

2. 定长线程池:FixedThreadPool

a.用来控制线程最大并发数。

b.如果没有空闲线程,剩下的任务会在队列等待有空闲线程执行。

3.支持定时和周期反复执行的定长线程池:ScheduledThreadPool

4.单线程的线程池:SingleThreadExecutor

线程池里只有一个线程,按照队列的出入规则来串行执行任务,例如先进先出。

线程池的大小设置多少合适?

线程池过大:大量线程竞争有限的cpu和内存资源,耗尽资源。

线程池过小:cpu利用率低,吞吐量低。

应当根据应用的特点来设计大小。

1.服务器的cpu多少?内存多大?任务是计算密集型还是IO密集型还是网络请求。

2.计算密集型的任务,在N个cpu的机器上大小为N或者N+1的线程池大小能实现最优使用率。

3.包含IO操作的任务或者IO密集型的任务,线程不会一直执行,会等待IO响应,可以把线程池设置得比N+1更大,用监控工具估算出计算任务和等待任务的比值,然后一步步微调,一步步试,看多大是性能最优的。

使用线程池的场景:

1、需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。

不使用线程池的场景:

1、如果需要使一个任务具有特定优先级

2、如果具有可能会长时间运行(并因此阻塞其他任务)的任务

3、如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)

posted @ 2019-09-21 19:07  纸飞机上的梦  阅读(673)  评论(0编辑  收藏  举报