线程池技术总结

1. 线程池解决了什么问题

线程池是集中管理线程的,以实现线程的重用,降低资源消耗,提高响应速度,提高线程的可管理性等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,他是一个用于统一创建与运行的接口。Executor框架实现的就是线程池的功能。使用线程池可以进行统一的分配,调优和监控。使用线程池的优势

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性

2. 线程池如何工作

使用线程池可以进行统一的分配,调优和监控。提交一个任务到线程池中,线程池的处理流程如下:

1、判断核心线程池里的核心线程(corePoolSize)是否已满

  • 是,判断工作队列是否已经满了
  • 否,不管核心线程有没有空闲,都创建一个新的线程执行这个任务

2、工作队列是否已经满了

  • 已满,判断线程池是否满了
  • 未满,将任务存储在工作队列里

3、判断线程池是否满了【核心线程以外的可以创建的线程,最大线程数】

  • 已满,交给饱和策略handle处理无法执行的任务
  • 未满,创建一个新的线程执行这个任务

4、调用executor()方法中创建一个线程,会让这个线程执行当前任务,线程在执行完当前任务以后,队列里有任务就会不停的从BlockingQueue中取任务执行。

线程池技术总结_第1张图片

(图片来自网络)

3. 线程池如何创建

线程池的真正实现类是 ThreadPoolExecutor,我们可以实现ThreadPoolExecutor 自定义线程池的方式,其次我们也可以使用Executor框架的 Executors类(java.util.concurrent包)创建封装好的线程。而ThreadPoolExecutor 类参数最多,并且 Executors 封装的线程池底层都是基于ThreadPoolExecutor实现的。

1、使用 Executors 工具类

什么是Executors?Executors框架实现的就是线程池的功能。Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。通过传入不同的参数,就可以构造出适用于不同应用场景

1.newFixedThreadPool 

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。

缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)

总结:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

2.newSingleThreadExecutor 

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力

总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它

3.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。如果工作线程空闲 60 秒没有被使用,会自动关闭。

特点:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的

长度作任何限制

缺点:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问

题的点,就可以去重写一个方法限制一下这个最大值

总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

4.newScheduledThreadPool 

创建一个定长线程池,支持定时及周期性任务执行。

特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于Timer(Timer是Java的一个定时器类)

缺点:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。

2、使用 ThreadPoolExecutor 

线程池技术总结_第2张图片

参数介绍如下:

  1. corePoolSize:线程池中的常驻核心线程数。如果调用了线程池的prestartAllCoreThreads 方法,线程池会提前创建并启动所有基本线程。
  2. maximumPoolSize:线程池允许创建的最大线程数,此值必须大于等于1。
  3. keepAliveTime:多余的空闲线程的存活时间。当前线程池的数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止。
  4. unit :keepAliveTime 的单位。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  5. threadFactory:用于设置创建线程的工厂,可通过该工厂给线程重命名等等。一般使用默认的即可。
  6. workQueue:等待队列,用于存放被提交但尚未执行的任务。
  7.  handler:拒绝策略。当队列满了,并且工作线程数量大于等于线程池的最大线程数(maximumPoolSize)时,拒绝请求执行的 runnable 的策略。

拒绝策略说明

  • AbortPolicy:直接抛出 RejectedExecutionException 异常来拒绝新任务的处理,这是默认策略。
  • CallerRunsPolicy:用调用者所在的线程来执行任务。调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果应用程序可以承受此延迟并且不能任务丢弃任何一个任务请求的话,可以选择这个策略
  • DiscardOldestPolicy:丢弃阻塞队列中最靠前(等待最久)的任务,并执行当前任务(ThreadPoolExecutor.execute(runnable))。
  • DiscardPolicy:不处理新任务,直接丢弃任务

3、注意事项

通过以上两种方式都可以得到一个线程池,日常开发中建议使用第 2 种方式来创建线程池。在阿里巴巴Java开发手册中提到不允许适用Executors创建线程,而是使用ThreadPoolExecutor方式明确线程池运行规则

线程池技术总结_第3张图片

4. 线程池如何关闭

我们可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,但是它们的实现原理不同。

1、shutdown 

线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。原理是只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

2、shutdownNow 

线程池拒接收新提交的任务,同时立刻关闭线程池,线程池里的任务不再执行。原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

只要调用了这两个关闭方法的其中一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow。

5.线程池如何配置线程数 

要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配

1、什么是CPU密集

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开

几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。

2、什么是IO密集

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,

即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

3、如何设置线程数

分配CPU和IO密集:

  • CPU 密集型任务配置尽可能少的线程数量,如配置 Ncpu+1 个线程的线程池。
  • IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如 2*Ncpu。

网上能很容易查到以上参考值,真正使用时候还需要考虑以下场景,并在观察一段执行情况后做出优化调整,经验永远大于理论

  • 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
  • 任务的依赖:是否依赖其他系统资源,如数据库连接等。
  • 任务的环境:CPU是几核的,CPU是否支持超线程,CPU是否也同时执行其他任务

前一篇:使用Arthas排查性能问题

你可能感兴趣的:(Java,java,开发语言)