线程池是我们常用到的功能之一,顾名思义,线程池就是存放线程的缓冲池,在一般的程序设计中,为了提高性能或处理较为复杂耗时的操作,我们一般开多个线程。线程池的目的就是管理这些线程,控制其生命周期以及任务分配等。
在写较深入的线程程序之前,我们一般会问,我们的任务(实现Runnable或Callable接口的类对象)被提交后是如何执行的,线程是如何被启动及终止的,线程池中的线程数是固定的吗,是一个线程运行一个任务还是一个线程运行多个任务的呢?
下面我们结合jdk的源代码来分析一下线程池的实现模型。
1. 生命周期
线程池的生命周期一共存在四种运行状态标志,分别为RUNNING、SHUTDOWN、STOP、TERMINATED,定义如下:
RUNNING:接受新任务并处理已排队(提交的任务是被放在一队列中)任务
SHUTDOWN:不接受新任务,但是处理已排队任务
STOP:不接受新任务,不处理已排队任务,并终止正在运行的任务
TERMINATED:与STOP相同,并加上所有任务均已终止
下面是各个状态之间的转换:
RUNNING -> SHUTDOWN:调用shutdown()函数
(RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()函数
SHUTDOWN -> TERMINATED:当任务队列和线程池均为空
STOP -> TERMINATED:当线程池为空
这个要注意一下,在调用执行器提交完任务之后,一定要调用执行器的shutdown()或者shutdownNow()方法来终止线程池,不然的话即使所有任务处理完毕,虚拟机也不会退出。
2. 调度过程
无论怎么写程序,线程池也是有其执行器创建的,并在其构造函数中确定了线程池大小,下面是ThreadPoolExecutor的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {…}
corePoolSize:核心线程池的大小,即即使线程空闲,也会保持这个数目
maximumPoolSize:线程池的最大大小,一般是Integer.MAX_VALUE,corePoolSize必须小于或等于这个数
workQueue:任务被执行之前放在这个队列中,线程要执行任务时从这个队列中取。注意BlockingQueue已经内置了同步机制,队列满或空均会使其处于对待状态。
其他参数不再说明。
通过上面大家可以看到线程和任务是分开存放管理的,这一点是理解线程池的关键。
下面我们通过一个函数看一下线程是如何被创建和启动的:
private void delayedExecute(Runnable command) {
//如果运行状态为SHUTDOWN,拒绝此命令
if (isShutdown()) {
reject(command);
return;
}
//如果当前线程数小于核心线程数,则创建并启动一个线程
if (getPoolSize() < getCorePoolSize())
prestartCoreThread();
//把任务加入到任务队列中
super.getQueue().add(command);
}
那么线程是如何执行任务的呢:
总体来说,提交的任务按照提交先后顺序被放入任务队列,线程启动后从队列中取出任务执行,任务被取出后即被从队列中删除。由于操作系统的调度和每个任务的运算量不同,因此单个线程取出的任务并没有顺序,但是一定是队列的头,每个线程都是平等的,不允许取头后面的任务。
下面是具体代码:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
//注意下面这个while循环,只要有任务就不会被终止,
//getTask()是取出任务队列的头,如果队列为空,进入阻塞状态
//runTask()是运行这个任务
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
更多的具体细节,请大家看ThreadPoolExecutor和ScheduledThreadPoolExecutor类 的源代码,两者都位于java.util.concurrent包中。