Java线程池配置由繁至简,找到适合自己的天命线程池(一)

2年前 (2022) 程序员胖胖胖虎阿
353 0 0

前提知识🧀

还记得刚入这行,还处于实习阶段的我,第一个项目就震撼到我了,因为发现自己熬夜苦读学习的知识和实际工作中需要的差别太大了,再加上项目用到的一些框架模块都很久,我连阅读代码的业务逻辑都很困难;其中让我印象深刻的就有一个封装了群发http请求的工具类,里面就用到了线程池,眼花缭乱的参数让那时的我头痛不已,有的参数甚至不知道是做什么用,为什么要设置成这个?

时间是让人猝不及防的东西,这么久终画上句。

免不了认识的7个基本参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}

贴上源码里的注释 并附加一些个人向的补充(参数加星代表比较重要):

*corePoolSize - 线程池中保留的线程数,即使它们处于空闲状态,除非设置 allowCoreThreadTimeOut 。

核心线程相当于合同工,有活儿干活儿,没活儿也得呆着,这个参数代表合同工(核心线程)的数量,int型

*maximumPoolSize - 池中允许的最大线程数 。

除了合同工,在有大量的工作堆积时,还可以找一些临时工来帮忙,这个参数代表总员工(合同工和临时工)数量的上限,int型

keepAliveTime - 当线程数大于核心时,这是多余的空闲线程在终止前等待新任务的最长时间。

临时工在没活儿的时候就遣散,这个参数代表多长时间没活干就遣散(销毁空闲线程),long型

unit – keepAliveTime 参数的时间单位 。

上面keepAliveTime参数的单位,在TimeUnit枚举中选择即可

*workQueue – 用于在执行任务之前保存任务的队列。(后面这句不用深究,可以不看)此队列将仅保存由 execute 方法提交的 Runnable 任务。

任务一直派,员工们干不过来,就设置一个队列存着这些任务;有好多种,下面会详细介绍

threadFactory – 执行程序创建新线程时使用的工厂 。

可以在这里给员工(线程)们命名之类的

*handler – 由于达到线程边界和队列容量而阻塞执行时使用的处理程序。

大多数文章会把它叫做拒绝策略,直译过来确实也没毛病,但新接触的人可能因为翻译的原因产生歧义;完整的含义是因为队列饱和所采用的处理程序:可能是拒绝,可能是丢弃,甚至可能不拒绝,会新建个线程继续跑任务,所以我们后面会沿用饱和策略的称呼,大家知道这两个称呼是同一个意思即可。

Java线程池配置由繁至简,找到适合自己的天命线程池(一)

几个重要参数的要求和相互之间的逻辑关系

如果以下其中一项成立,将会抛出 IllegalArgumentException

  • corePoolSize < 0
  • keepAliveTime < 0
  • maximumPoolSize <= 0
  • maximumPoolSize < corePoolSize

上面是比较常规的要求,一句话说就是最大线程至少为1,并且要大于核心线程数量。
threadFactory 和 handler 不是必填参数,两者都会有默认值,所以一些构造方法可能只用到其他5个参数。

常用的几个任务队列

为了更清晰地认识线程池,我们要大致介绍一下:

  • ArrayBlockingQueue

看到Array开头,我们就知道这个队列是使用数组实现的队列。

  • LinkedBlockingQueue

这个以Linked开头,大家比较熟悉以此开头的有LinkedList,其实这个队列就是用链表实现的队列。

有的文章会把ArrayBlockingQueue叫做有界队列,把LinkedBlockingQueue叫做无界队列,对此我只想说:有一点误导人。

因为两者说白只有底层实现不同,我们知道数组在内存是连续的,所以需要规定大小,链表可以不连续,所以理论上可以无限延长,但也不代表就一定是无界的。
LinkedBlockingQueue有一个参数叫capacity,就是代表队列的容量,无界的原因是用了无参构造,capacity就默认为Integer.MAX_VALUE,但就像list和map一样,你可以在一开始就设置你想要的容量。

//无参构造
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

//预设最大容量的构造
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

这样横向一比较,LinkedBlockingQueue的吞吐量比ArrayBlockingQueue要高,可以跟ArrayBlockingQueue一样规定最大容量,也可以无界;这么一比,LinkedBlockingQueue完胜,所以你只要了解这个逻辑,这俩任务队列相比之下肯定用LinkedBlockingQueue。

ArrayBlockingQueue的存在更像是用来突出LinkedBlockingQueue更好用。

(ArrayBlockingQueue:我没惹你们任何人!
Java线程池配置由繁至简,找到适合自己的天命线程池(一)

  • SynchronousQueue

除去上面两个队列外,还有这个比较特殊的队列,因为它没有容量,或者说容量为0,它的每一个put 操作必须等待一个take 操作,也就是它的上限在于take 操作的效率,也就是工作线程的效率。这个队列在当有足够多的消费者时,是最合适的队列。
换句话说,假如你需要线程池去处理的任务数不多,qps不高,甚至峰值也不高,未来也不会有大的变化,那恭喜你,你已经找到了你的真命线程池,直接使用Executors.newCachedThreadPool(),它完全能胜任你的需求,甚至对原因不太在意的同学,可以马上关掉页面用起来了。

罢特,我也相信,这种情况还是少,大部分人都是因为需要线程池来“兜底”,也就是任务数或者任务峰值线程池真的撑不住,才来查询怎么找到适合自己的配置,那咱们不慌,就继续往下看。
Java线程池配置由繁至简,找到适合自己的天命线程池(一)

阿里巴巴Java开发手册为什么不推荐使用Executors类自动生成的几个线程池

上面提到了Executors.newCachedThreadPool(),Executors相当于对线程池的一个工具类,系统提供了几个参数已经预设好,一行代码就可以创建的线程池供开发者使用,但是_《阿里巴巴Java开发手册》_里却不推荐使用,这是为什么呢?
我们先来看一下上面提到的,通过Executors类创建的线程池newCachedThreadPool:

//创建一个系统预设好的线程池
ExecutorService executorService = Executors.newCachedThreadPool();

//构造函数如下
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, 
                                  Integer.MAX_VALUE,
                                  60L, 
                                  TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

我们可以看到:核心线程为0,最大线程数为MAX(可以理解无上限),任务队列使用的是SynchronousQueue,乍一看好像没什么问题,那为啥手册里不推荐使用呢?

手册上是这么说的,我们直接看看2):
Java线程池配置由繁至简,找到适合自己的天命线程池(一)
什么意思呢?就是说这个预设好参数的线程池CachedThreadPool,它的最大线程数是Integer.MAX_VALUE,我们可以理解为最大线程数无上限,当生产者提交任务的量攀升,消费者处理不过来,就会不停地添加工作线程,因为线程数没有上限,会不停地添加线程,直到发生OOM。

看到这里我相信你已经知道为啥上面推荐使用CachedThreadPool时要加那么多的前置条件了。
因为一旦消费者处理不过来,就有引起OOM的风险存在,谁又敢乱用呢。

而且在知道这个后我们可以举一反三,还有什么会因为任务数变多而骤增,进而也会发生OOM呢,没错,就是任务队列数。

所以只要以下条件满足一个,在任务处理不过来的情况下就有可能发生OOM:

  • maximumPoolSize为Integer.MAX_VALUE(或很大
  • workQueue为无界队列(或很大

然而Executors这个工具类预设的几个线程池,不是最大线程数是Max,就是任务队列是无界的,都满足上面的条件,所以系统预设的线程池,手册都不建议使用

手册的意思很明了了,都不建议使用的意思其实就是:

要根据项目,自己来设置合适的参数。
Java线程池配置由繁至简,找到适合自己的天命线程池(一)

写在最后的最后

因为篇幅问题,我们这篇文章只是开个头,讲述一些基本参数,引出最后的问题。因为我觉得一篇文章的字数大概在2000~3000字比较合适,内容太多的话,可能不太好接受,不太好吸收;不过放心其实后面一篇也已经完成,只剩一点修修补补的工作,马上就会发上来。
但也许有人会觉得比较啰嗦,但也没差啦,我也是跟着自己的feel来的,比如每个标题及其内容,我也是反复阅读过好几次才排好顺序,循序而进,可能不是把基础全铺上再来给结论,而是引导一个又一个的问题来讲述,如果你能有所收获,那么我会很开心的~如果可以点赞评论收藏分享,那么我的动力会更足的,谢谢大家~
Java线程池配置由繁至简,找到适合自己的天命线程池(一)

相关文章

暂无评论

暂无评论...