文章摘要
GPT 4
此内容根据文章生成,并经过人工审核,仅用于文章内容的解释与总结

前提知识 🧀

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

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

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

1
2
3
4
5
6
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 – 由于达到线程边界和队列容量而阻塞执行时使用的处理程序。
大多数文章会把它叫做拒绝策略,直译过来确实也没毛病,但新接触的人可能因为翻译的原因产生歧义;完整的含义是因为队列饱和所采用的处理程序:可能是拒绝,可能是丢弃,甚至可能不拒绝,会新建个线程继续跑任务,所以我们后面会沿用饱和策略的称呼,大家知道这两个称呼是同一个意思即可。

给你小发发

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

如果以下其中一项成立,将会抛出 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 一样,你可以在一开始就设置你想要的容量。

1
2
3
4
5
6
7
8
9
10
11
//无参构造
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:我没惹你们任何人!
我没惹你们任何人!

  • SynchronousQueue

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
//创建一个系统预设好的线程池
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,就是任务队列是无界的,都满足上面的条件,所以系统预设的线程池,手册都不建议使用

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

要根据项目,自己来设置合适的参数。
未完待续

写在最后的最后

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