0.背景
在 Java 早期,每次创建线程时,都要涉及到线程的创建、销毁以及资源管理,这对于系统的性能和资源利用率是一种浪费。
因此,Java 提供了线程池的概念,以提高线程的管理效率和性能。
-
资源管理优化:传统的线程创建和销毁需要涉及系统资源的分配和释放,而频繁的线程创建和销毁会导致资源的浪费和性能下降。
线程池通过维护一组可重用的线程来减少线程的创建和销毁次数,从而优化了系统资源的利用。
-
线程复用:线程池可以维护一定数量的线程池,这些线程可以被重复利用来执行多个任务,而不必每次都创建新线程。
这样可以避免频繁地创建和销毁线程,提高了系统的性能和效率。
-
任务调度和管理:线程池可以对任务进行调度和管理,可以根据任务的优先级和类型来调度执行线程,提供了更加灵活和可控的任务执行方式。
-
并发控制:线程池提供了一种方便的方式来控制并发执行的线程数量,可以限制同时执行的线程数量,从而避免因线程过多导致系统资源耗尽或性能下降的问题。
1.概念
1.1 关键词
记忆方法:核大存单队略厂(这里和大存单的队伍略长)
1.1.1 核心线程数 corePoolSize
- 含义:指定线程池中保持的活动线程数。除非设置了
allowCoreThreadTimeOut
为true
,否则这些线程不会被回收。 - 作用:核心线程数决定了线程池中保持的最小活动线程数,即使线程处于空闲状态也不会被回收。
1.1.2 最大线程数 maximumPoolSize
- 含义:指定线程池中允许创建的最大线程数。当任务队列已满且核心线程数已达到最大时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。
- 作用:最大线程数决定了线程池中允许创建的最大线程数,用于处理任务队列中的任务。
1.1.3 空闲线程存活时间 keepAliveTime
- 含义:指定空闲线程的存活时间,即当线程池中的线程处于空闲状态且空闲时间超过该值时,这些空闲线程可以被回收。
- 作用:通过设置线程的存活时间,可以控制线程池中线程的数量,避免因线程过多导致系统资源的浪费。
1.1.4 空闲线程存活时间单位 unit
- 含义:指定空闲线程存活时间的单位,可以是纳秒、微秒、毫秒、秒、分钟、小时或天等。
- 作用:单位参数用于指定空闲线程存活时间的时间单位,与
keepAliveTime
参数配合使用。
1.1.5 工作队列 workQueue
- 含义:任务队列,用于保存等待执行的任务。线程池中的线程会从任务队列中取出任务执行。
- 作用:工作队列用于存储提交给线程池但尚未执行的任务,当线程池中的线程处于忙碌状态时,新的任务会被放入任务队列等待执行。
1.1.6 拒绝策略 rejectedExecutionHandler
- 含义:拒绝策略,用于处理当任务无法被线程池执行时的情况。当任务队列已满且无法接受新的任务时,或者线程池已关闭但仍然有任务提交时,会触发拒绝策略。
- 作用:拒绝策略定义了线程池无法处理任务时的行为,包括抛出异常、丢弃任务、阻塞调用者或者执行任务的线程等待一段时间后再尝试执行任务等。选择合适的拒绝策略能够有效地处理任务提交过载的情况,保护线程池和系统的稳定性。
1.1.7 线程工厂 threadFactory
- 含义:线程工厂,用于创建新的线程。当线程池需要创建新线程时,会调用线程工厂的
newThread(Runnable r)
方法来创建线程。 - 作用:线程工厂用于创建线程池中的线程实例,通过自定义线程工厂,可以控制线程的创建过程,如设置线程的名称、优先级、是否为守护线程等。
1.2 线程池的状态
线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。
运行状态 | 是否接收任务 | 是否处理任务 | 状态描述 |
---|---|---|---|
RUNNING | √ | √ | 能接受新提交的任务,并且也能处理阻塞队列中的任务。 |
SHUTDOWN | × | √ | 不再接受新提交的任务,可以继续处理阻塞队列中已保存的任务。 |
STOP | × | × | 不再接受新任务,也不继续处理队列中的任务,会中断正在处理任务的线程。 |
TIDYING | - | - | 所有的任务都已终止了,workerCount(有效线程数)为0。 |
TERMINATED | - | - | 终止状态,在 terminated() 方法(预留)执行完后进入该状态。 |
状态转换如下:
1.3 运行过程
有核心线程,优先用核心线程。核心线程不够用,先往工作队列里放。工作队列都放不下了,再创建非核心线程来执行。实在是不行了,只能进行拒绝。
-
开始,接收到提交的一个任务。
-
判断线程池是否还在运行
-
如果不是RUNNING状态,则不再接受任务提交,进行拒绝策略。
-
如果是RUNNINNG状态,可以接收任务提交。
判断当前工作线程数与核心线程数的关系
-
小于核心线程数,新建工作线程(核心)并执行。
-
大于等于核心线程数
检查工作队列情况
-
工作队列未满,添加任务到工作队列中,等待获取执行。
-
工作队列已满
检查最大线程数
- 未达到最大线程数,新建工作线程(非核心)执行。
- 已经达到最大线程数,进行任务拒绝。
-
-
-
1.4 理解
注意,以下是我的理解,我半桶水,你记得带自己的思考来阅读喔。
1.4.1 核心线程数和最大线程数
忽略核心线程是否可以销毁
核心线程必定在运行,是我们使用的最小成本。而最大线程是一个兜底机制,不可能无休止的来创建线程,必定要有个度。
线程池中的任务,是依赖核心线程和非核心线程来执行的,不是说跟线程创建销毁跟工作队列没关系,而是侧重点不同,工作队列的目的是缓冲。
1.4.2 工作队列存在的意义
工作队列的存在,是为了提供一个缓冲,帮助线程池保持一个最小可运转的线程数量,避免频繁的创建和销毁线程。
好,我们回顾上面的图,核心线程没满的时候,我们可以新建线程来执行,在核心线程内的数量,我们是很好接受的。
那么,当我的核心线程都不够用了,这个时候,暴力的方式当然是直接新建线程来执行。
- 优点,核心线程在忙碌,我直接搂一个新线程,立马就能执行,效率很高。
- 缺点,涉及到的频繁的创建线程(高成本)。
有了工作队列后,我们核心线程都在忙碌时,选择先将任务放到队列里等一等,实在再放不下了,再来创建线程执行。
我们还可以极端点,假设我们新建一个线程要2s,执行一个任务只要0.5s。
核心线程已满,这个时候,我们是不是等个0.5s就能空闲出一个核心线程,然后就有得执行了。而不是选择立马新建一个,耗费2s。
1.4.3 ThreadPoolExecutor分析
1.4.3.1 关于参数校验
经常会有面试问,核心线程数能设置为0吗?
注意这是构造方法,是做了校验的。
A.参数不合法异常 IllegalArgumentException
- 核心线程数 < 0
- 最大线程数 <= 0
- 最大线程数 < 核心线程数
- 线程存活时间小于0
B.空指针异常NullPointerException
- 工作队列为null
- 线程工厂为null
- 拒绝策略为null
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这个构造函数,最少需要5个核心参数喔。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
1.4.3.2 核心线程数可以为0吗?
可以
线程池也可以接收任务,线程池会依赖非核心线程来执行任务。
任务首先会被放入工作队列进行缓冲,如果工作队列已满,再根据最大线程数的限制来新建非核心线程来执行任务。
只有在工作队列已满,且线程池中的线程数未达到最大线程数时,才会创建新的线程,如果线程池已达到最大线程数,则不会再创建新线程,根据拒绝策略来处理任务。
然后呢,这个时候也有个问题,由于我们没有核心线程,直接被放到工作队列里,这个队列没满的话,就不会创建线程,相当于在干等。
嗯,就跟我们写的代码一样,让人感到无语。
1.4.3.3 最大线程数可以为0吗?
不可以
首先通过前面可以知道,最大线程数必须比核心线程数大,然后呢,核心线程数不能小于0(参数异常),但是核心线程数可以等于0。
然后呢,还有个校验,是最大线程数是必须大于核心线程数的,所以说呀,这个最大线程数,最小值是1。
哦,sorry,最大线程数的判断直接是maximumPoolSize <= 0
抛异常。
我没骗你,你自己看图。
1.4.4 执行流程代码简述
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
2.实现
2.1 Executor接口
- 定义了一个单一的方法
execute(Runnable command)
,用于执行提交的任务。 - 只关注任务的提交方式,不关心任务的执行细节。
2.2 ExecutorService接口
- 是
Executor
接口的子接口,提供了更多的方法用于任务管理和线程池控制。 - 定义了一系列方法来提交任务、关闭线程池、管理任务的执行状态等。
2.3 Executors类
- 是一个工具类,提供了一系列静态方法用于创建不同类型的线程池。
- 通过
Executors
类可以快速方便地创建各种预定义类型的线程池,如固定大小线程池、可缓存线程池等
2.4 ThreadPoolExecutor类
- 是
ExecutorService
接口的一个具体实现类,也是Executors
类创建线程池的底层实现。 - 提供了一个灵活的线程池实现,可以通过构造方法来设置各种参数,如核心线程数、最大线程数、任务队列、拒绝策略等。