前言
作为java程序员,练习时候经常会写到线程,而实际在生产项目中更偏向于使用线程池,所以笔者就研究一下线程池的实现。
正文
首先看类结构图
从最下层的ThreadPoolExecutor类看继续了AbstractExecutorService这个抽象类,而AbstractExecutorService实现了ExecutorService接口。ExecutorService接口继承了Executor接口。
顶层接口Executor就很简单,就定义一个execute执行方法。
void execute(Runnable command);
从这方法了解到,单单的一个execute是不能满足众多程序猿的需求,于是就出现了ExecutorService接口。
显而易见的可知ExecutorService继承了Executor并对他进行了很多的扩展,定义了许多方法。在ExecutorService中还增加了对Callable和Future的调用,读者可以简单理解一下,Callable是可拥有返回结果的线程,对应的Future就是用来接收Callable执行的返回结果,和runnable不同runnable创想的线程运行时没有返回结果的。
作为ExecutorService的实现类AbstractExecutorService,具体实现了ExecutorService接口中的方法。
ThreadPoolExecutor作为一个AbstractExecutorService的子类,且不是抽象类,算是线程池的具体实现类。可能很多读者很少使用ThreadPoolExecutor去创建线程池。更偏向于使用Executors工具类去创建。
举个例子
ExecutorService executor = Executors.newSingleThreadExecutor();
简单,方便,实用,无需输入参数就可创建,虽说这样方便了开发者的使用,但也造成了风险,对线程池不正确的创建很容易造成内存泄露的问题。
比如上个例子中提到的Executors.newSingleThreadExecutor()
创建单个线程的线程池方法。
观察一下它的方法代码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
参数定义:
- 第一个参数是线程池的核心线程数
- 第二个参数是线程池的最大线程数
- 第三参数是线程的存活时间
- 第四个参数是时间单位
- 第五个参数是等待队列
其实还有拒绝策略这个参数,不过参数的详细含义会在后文介绍。
从代码可知,newSingleThreadExecutor方法是new一个ThreadPoolExecutor。
来看最后一个参数LinkedBlockingQueue,声明做为一个阻塞的无界队列。
如果出现大量请求访问这个线程池的场景,因为只有一个线程进行处理,就会有大量的请求进入队列去排队,LinkedBlockingQueue参数又声明成了一个无界队列,就会出现一种场景越来越来的请求堆积到队列中,最后导致这个队列吃光了所有内存从而导致内存溢出。
所以笔者推荐使用线程池的时候显示调用ThreadPoolExecutor的构建方法,这样虽然需要你输入各个参数,但也会加强你对线程池的理解,可以有效避免之前提到的那种场景。
常量定义
老方法,先看有哪些常量
// 核心线程数
private volatile int corePoolSize;
// 最大线程数
private volatile int maximumPoolSize;
// 空闲线程的存活时间
private volatile long keepAliveTime;
// 拒绝策略
private volatile RejectedExecutionHandler handler;
// 等待队列
private final BlockingQueue<Runnable> workQueue;
// 线程工厂
private volatile ThreadFactory threadFactory;
-
核心线程数,即在线程池执行任务的时候会启动线程,即便有其他空余线程,线程池依然会启动一个线程来执行,直到线程内的线程数达到核心线程数的时候,这时候进来的任务就不会启动新的线程去处理,使用其他的空余核心线程去处理。
-
最大线程数,一个线程池的允许运行的最大线程数量。达到最大线程数是有前提的。先讲下线程池的工作流程
- 一个任务进入线程池,判断当前池内是的线程是否小于核心线程数,小于的话创建一个新线程去执行任务。
- 如果池内线程已达到核心线程数,则进入workQueue等地队列进行排队。
- 如果等待队列是个有界队列且任务满的时候,这时候还有任务进到线程池来,就创建额外的线程去执行任务。
- 如果创建额外的线程+核心线程已达到最大线程数了,这时候依然还有任务进来,这时候就会调用拒绝策略,来对这些请求线程池的任务进行处理。
所以如果等待队列是个无界队列,那maximumPoolSize参数就毫无意义。
-
keepAliveTime 空闲线程的存活时间,默认情况下,只有才线程数超过核心线程数才会生效。
-
workQueue等待队列,存储任务超过核心线程数的任务,也就是所有核心线程都在运行了,还有任务进来就进入等待队列。
-
threadFactory 自定义实现线程的命名方式,通常我们使用默认的线程工厂就足够了,
Executors.defaultThreadFactory()
-
handler 拒绝策略,当任务超过最大线程数的时候,对多出的任务进行处理,线程池提供了4种拒绝策略。
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
// 第二部分,线程池状态的常量定义
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// COUNT_BITS 等于整型的最大位数-3 即32-3=29;
private static final int COUNT_BITS = Integer.SIZE - 3;
// 线程池的最大容量为2的29次方-1 大概500多万
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// int 类型为32位数据,高三位用来存储线程池的状态,剩下29位用来表达线程池的容量
// 运行状态
private static final int RUNNING = -1 << COUNT_BITS;
// 停止运行状态,此时线程池
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
没错,线程池是有状态的。running 状态字面意思理解就是正常运行的状态,其他四个皆为停止状态,线程池的停止是一个循序渐进的流程,每个流程都拥有不同的状态和做不同的工作。
停止线程的流程
1.Running->SHUTDOWN,正常运行的线程池调用shutdown()方法后,线程池的状态就会变更为SHUTDOWN。当进入SHUTDOWN时,就会停止接收新的任务,而队列中正常正在等待的任务还会正常执行。
2. Running or SHUTDOWN -> STOP, 调用shutdownNow()方法后,线程池会进入stop状态,不仅会停止接收任务,正在运行的任务也会进行中断。
3. SHUTDOWN -> TIDYING:当任务队列和线程池都清空后,会由 SHUTDOWN 转换为 TIDYING
4. TIDYING -> TERMINATED:状态为TIDYING, 会回调terminated() 方法, 当 terminated() 方法结束后,状态变更为TERMINATED。
后话
本文介绍了线程池的工作流程和一些常量定义,以及线程池的类结构关系。