vlambda博客
学习文章列表

日常回顾——Java线程池123

前言

一年之计在于春,意指一年的计划要在春天考虑安排。立春已过,兄die,今年的Flag你立了吗?


前些天在让同事帮忙Review代码的时候,他针对我定义的一个线程池提出了如下一段修改意见:

“线程池这样配最大线程数基本没用,因为超过10个就会进队列,队列还是无限大,如果性能能满足就无所谓,需要提高性能的话可以考虑SynchronousQueue“

日常回顾——Java线程池123

基于他的这段意见,这两天花了点时间复习了一下Java线程池相关的内容,整理了一下在这里分享给大家。


本篇公号主要包含以下几个方面的内容:

  • 1. 阿里Java编程规约-线程池相关

  • 2. 四种常用的线程池、阻塞队列拒绝策略

  • 3. ThreadPoolExecutor概述

  • 4. 如何确定线程池的大小


废话不多说,干货分享,大佬绕道。日常回顾——Java线程池123

This browser does not support music or audio playback. Please play it in Weixin or another browser. 日常回顾——Java线程池123

一、阿里Java编程规约-线程池相关

咱们先来复习一下阿里关于线程池相关的编程规约:


  • 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:

public class TimerTaskThread extends Thread {
 public TimerTaskThread() {
  super.setName("TimerTaskThread");...
 }
}
  • 【强制】线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。
如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过渡切换” 的问题。
  • 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:

1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

二、四种常用的线程池、阻塞队列、拒绝策略

虽然阿里不允许我们使用Executors去创建线程池,但还是有必要了解一下:

  • 四种常用的线程池

        a.newFixedThreadPool

创建一个有固定数量线程的线程池,其corePoolSize=maximumPoolSize,且keepAliveTime为0,适合线程稳定的场所。

public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

         b.newCachedThreadPool

线程池的大小不固定,可灵活回收空闲线程,若无可回收,则新建线程,其corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE

public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());

        c.newScheduledThreadPool

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

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
  return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
  super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
        new DelayedWorkQueue());
}

        d.newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
  return new DelegatedScheduledExecutorService
      (new ScheduledThreadPoolExecutor(1));
 }
  • 四种常用的阻塞队列

1、ArrayBlockingQueue:线程池的缓存队列是一个有界队列,实质是一个定长数组,不允许动态改变数组长度。
2、PriorityBlockingQueue:优先级队列,基于可变长数组的无界队列,底层用最小堆实现,该队列定义的时候要么传入比较器,要么其任务对象类需要实现comparable接口。
3、LinkedBlockingQueue 是基于链表结构的有界阻塞队列,大小(Integer.MAX_VALUE)。使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
4、SynchronousQueue 无缓冲的等待队列,无界;只有在使用无界线程池或者有饱和策略时才建议使用该队列
  • 四种常用的拒绝策略

AbortPolicy:直接拒绝策略,也就是不会执行任务,直接抛出RejectedExecutionException,这是默认的拒绝策略。
DiscardPolicy:抛弃策略,也就是直接忽略提交的任务(通俗来说就是空实现)。
DiscardOldestPolicy:抛弃最老任务策略,也就是通过poll()方法取出任务队列队头的任务抛弃,然后执行当前提交的任务。
CallerRunsPolicy:调用者执行策略,也就是当前调用Executor#execute()的线程直接调用任务Runnable#run(),一般不希望任务丢失会选用这种策略,但从实际角度来看,原来的异步调用意图会退化为同步调用。

三、 ThreadPoolExecutor概述

Executors利用工厂模式实现的四种线程池,选择使用Executors提供的工厂类,就会忽略很多线程池的参数设置,工厂类一旦选择设置默认参数,就很容易导致无法调优参数设置,从而产生性能问题或者资源浪费。所以,这也是阿里为啥建议我们使用ThreadPoolExecutor自我定制一套线程池。

  • ThreadPoolExecutor溯源

日常回顾——Java线程池123

  • 详细参数介绍:

public ThreadPoolExecutor(
  int corePoolSize,// 线程池的核心线程数量
  int maximumPoolSize,// 线程池的最大线程数
  long keepAliveTime,// 当线程数大于核心线程数时,多余的空闲线程存活的最长时间;也即非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长
  TimeUnit unit,// 时间单位
  BlockingQueue<Runnable> workQueue,// 任务队列,用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。
  ThreadFactory threadFactory,// 线程工厂,用来创建线程,一般默认即可
  RejectedExecutionHandler handler// 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务,默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。
)
 
  • 线程池分配流程日常回顾——Java线程池123

日常回顾——Java线程池123

四、如何确定线程池的大小

一般多线程执行的任务类型可以分为CPU密集型I/O密集型,根据不同的任务类型,我们计算最佳线程数的方法也不一样。下面直接给出理论性的结论吧:

使用多线程的主要目的我们应该都能回答的出来就是提高程序的性能,这个提高性能其实是指降低延迟(发送请求到接收到数据的时间)和提高吞吐量(单位时间能可以处理更多的请求)。
降低延迟和提高吞吐量对应的方法有两种: 优化算法 和 将机器的硬件性能发挥到极致
优化算法:降低时间和空间复杂度,使得程序执行时间更短。
将硬件的性能发挥到极致,具体的指提高I/Ocpu的利用率
如果我们一段程序有IO操作和Cpu计算,我们可以称之为:IO密集型任务。程序中没有IO操作只有Cpu的话 称之为Cpu密集型任务。
  • CPU密集型任务

最佳线程数 = Ncpu(CPU核数)+1
  • I/O密集型任务

最佳线程数 = Ncpu(CPU核数) * [ 1 +(I/O耗时/Cpu耗时)]
也有一种粗略的算法,默认(I/O耗时/Cpu耗时)趋近于1,于是:最佳线程数 = Ncpu(CPU核数)*2

参考:

只会一点java :https://cnblogs.com/dennyzhangdd/p/6909771.html
小鱼儿_karl:cnblogs.com/karlMa/p/11356041.html
金山刘超Java性能调优系列——18|如何设置线程池大小?
一次Java线程池误用引发的血案和总结 https://zhuanlan.zhihu.com/p/32867181

总结

“这世界本就没有任何一句话,可以让你醍醐灌顶。真正叫你醍醐灌顶的,只能是一段经历。而那句话,只是火药仓库内划燃的一根火柴。”


欢迎大佬指正,创作不易,喜欢的朋友,记得点赞👍关注➕