前言
好久没更新文章了,不是去偷懒了,而是自己又开了一个课题(Spring)。然后学习的过程中心态被搞爆炸了,所以写写其他的东西缓解一下心情。等 Spring 整理完了,会出相应的文章。
线程池背景
一个应用,我们肯定要多次使用线程,那也就意味着我们要多次创建销毁线程。而创建并销毁线程的过程势必会消耗内存。而在 Java 中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
当然,使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
线程池的参数结构
要想用好线程池,我们一定要了解它的底层设计。那么今天,让我和大家一起来梳理一下吧。
通过源码我们可以发现线程池系列都是套娃 ThreadPoolExecutor 方法的。所以搞清楚 ThreadPoolExecutor 才是弄懂线程池的关键。
public ThreadPoolExecutor(int corePoolSize, //核心线程数限制
int maximumPoolSize,//最大线程限制
long keepAliveTime,// 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue,// 任务队列
ThreadFactory threadFactory,// 线程工厂
RejectedExecutionHandler handler) {// 拒绝策略
复制代码
这 7 个参数请务必记下来。
下面这张图是线程池的运行机制:
结合这张图,我们来进一步了解各个参数的含义。
线程数
线程分为核心线程和非核心线程,非核心线程数 = 最大线程数 – 核心线程数。
简单帮大家梳理一下线程池的工作流程:
- 如果有任务,首先要分配给核心线程的。
- 如果核心线程没有空闲,都有活干,那么再存到阻塞队列。
- 如果阻塞队列满了,再分配给最非核心线程。
- 如果非核心线程没有空闲则采取拒绝策略。
空闲线程存活时间
keepAliveTime, 空闲线程存活时间,当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值
任务队列 BlockingQueue
BlockingQueue不能够添加null对象,否则会抛出空指针异常。
常见的任务队列有
- LinkedBlockingQueue
基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
- ArrayBlockingQueue
是一个用数组实现的有界阻塞队列,按FIFO排序量。
- SynchronousQueue
同步队列,一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
- PriorityBlockingQueue
是带排序的 BlockingQueue 实现,队列为无界队列。
记住我标粗的内容,后面会有大用。
线程工厂
在使用线程池时,强烈推荐使用自己定义的线程工厂,这样能为线程池中的线程进行命名,方便跟大家使用 jsatck 命令查看线程栈时,能快速识别对应的线程。
拒绝策略
拒绝策略一共有四种 AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy 和 DiscardPolicy。那么我们分别介绍一下这四种拒绝策略:
- AbortPolicy:默认策略,如果当前线程池的状态不是 RUNNING,队列已满,当前线程最大数也达到了最大值,再来任务会抛异常。
- CallerRunsPolicy:此拒绝策略由调用线程(提交任务的线程)直接执行被丢弃的任务。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
如何合理地配置线程池参数
现在去百度会有两个分别关于 IO 密集型和 CPU 密集型的计算公式,但我看了很多大佬博客发现他们都不是很赞同这样的观点,所以最后的结论是:以本人 4 核机器:
线程数 = CPU核心数/(1-阻塞系数)
阻塞系数通常在(0.8 ~ 0.9)。阻塞系数取 0.8 ,线程数为 20 。阻塞系数取 0.9,大概线程数40,20个线程数我觉得可以。下图是美团到店研发所用的线程池参数,我们可以借鉴一下。
值得注意:所谓 CPU 密集型,是指纯计算类,例如计算圆周率的位数,当然我们基本接触不到,我们接触的业务还是要通过请求和数据库,中间件等去进行关联。如果非要说 CPU 密集型,那么 CPU 密集型不宜设置过多线程,因为是会造成线程切换,反而损耗性能。比上面公式得出的结果小一些就可以吧。(这里确实没找到合适资料,毕竟参数设置还是需要根据具体业务来判定)
顺便说下,如何得到当前服务器核心线程数:
System.out.println(Runtime.getRuntime().availableProcessors());
复制代码
本人电脑结果为 4 核:
简单入门
我们都知道,线程池一共有 5 种,其中 2 种为定时类型的,不是我们这次讨论的目标。这次主要探讨另三种,分别为 newFixedThreadPool,newSingleThreadExecutor 和 newCachedThreadPool。先看一个简单小例子。
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService es1 = Executors.newCachedThreadPool();
ExecutorService es2 = Executors.newFixedThreadPool(10);
ExecutorService es3 = Executors.newSingleThreadExecutor();
es1.execute(new MyTask(i));
//es2.execute(new MyTask(i));
//es3.execute(new MyTask(i));
}
}
class MyTask implements Runnable {
int i = 0;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
了解完小例子,我们分别介绍一下这三种线程池。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
复制代码
通过源代码我们可以看出 newCachedThreadPool 的 核心线程数为 0,最大线程数为 Integer.MAX_VALUE(即 2^31 – 1 = 2147483647,21亿多,没有哪台机器可以承受吧,所以可以理解为不受限制),然后线程的生命时长为 60s 重点是任务队列为 BlockingQueue。
如图我们可以看出,线程池中的所有线程都是在非核心线程区创建的(由于核心线程区为 0),相当于假如我有100份工作,而我又能找到 100 个工人帮我去做,那么很快就可以完成,但我们又知道,线程数和 cpu 息息相关。由于创建线程没有限制,那么所有的线程都在非核心线程区创建,CPU 会消耗大量性能去创建线程,所以 newCachedThreadPool 极有可能造成 CPU 100%。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
复制代码
核心线程数和最大线程数都是传入参数,空闲线程存活时间为 0s(keepAliveTime 为非核心线程使用完后会多久回收,由于这里没有设置非核心线程数,所以线程使用完不回收。),注意这个任务队列为 LinkedBlockingQueue。LinkedBlockingQueue 内部基于链表来存放元素,它如果不指定容量,默认为 Integer.MAX_VALUE,也就是无界队列。(通常用的时候会设置大小,而这里没设置,为默认值。)。但是 newFixedThreadPool 有个问题,由于没有设置 LinkedBlockingQueue 的初始大小,所以它会无限大,极有可能会造成 OOM。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
复制代码
除了核心线程数和最大线程数为 1 以外,其他的和 newFixedThreadPool 一样。也就是说它是一个任务一个任务的进行,所以 newSingleThreadExecutor 是三个里最慢的。当然了,newSingleThreadExecutory 依然有发生 OOM 的问题。
阿里开发手册强制使用自定义线程池。
线程池的状态
聊完了参数和用法,我们再聊聊线程池的状态。线程池有五种状态 RUNNING、SHOUTDOWN、STOP、TIDYING 和 TERMINATED.
我们简单聊一下 showdown 和 shutdownNow 方法吧,当我们任务队列中还有任务时,如果调用 showdown 方法,我们会将线程池中和工作队列中的任务都执行完再转换为 TIDYING 状态,再结束,比较友好;而 shutdownNow 只会将当前线程池中的任务执行完毕,不会再管工作队列中是否还有任务,就结束,比较暴力些。
总结
本篇我和大家一起简单了解一下线程池的原理,其中线程池的配置参数尤为重要,如何合理的配置也需要格外关心。同时,有简单展示创建线程池,以及线程池原生 api 的底层逻辑,并叙述了为什么不推荐使用原生 api 而使用自定义线程池。下图是线程池的工作流程,简单明了。源码会在下一篇展示,马上奉上。
站在巨人肩膀
-
【Java线程池实现原理及其在美团业务中的实践】tech.meituan.com/2020/04/02/…
-
B站的源码讲师小刘 @暴躁小刘 up主主页:space.bilibili.com/457326371?f…
-
【看看面包超人的 ‘招牌线程池’ 用得可还行?】mp.weixin.qq.com/s/tvEwOpVH-…
-
面试官:你是如何评估一个线程池需要设置多少个线】mp.weixin.qq.com/s/3E6qdOci-…