在 并发编程 中,我们只介绍了几种常用的线程池,以及它们的使用,那线程池是如何管理一组线程的呢?
介绍
先来看下关系图:
图片来源网络
从图中我们可以看出,之前我们学习的几个常用的线程池 CachedThreadPool、FiedThreadPool 等都是通过 Executors 调用创建的:
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(MyRunnable);
而 Excutors 其实是一个工具类,为我们提供了创建三种典型线程池的方法,内部使用的是 ThreadPoolExcutor,最后调用的 ThreadPoolExecutor 的构造方法来创建线程池。
ThreadPoolExcutor
ThreadPoolExcutor 是线程池体系中的核心,那么先来看下它的构造方法以及构造参数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize: 线程池中核心线程的数量;当提交一个任务到线程池的时候,线程池会创建一个线程来执行执行任务,即使有其他空闲的线程存在,直到线程数达到corePoolSize时不再创建。
- maximumPoolSize: 线程池允许创建的最大线程数;如果阻塞队列已经满了,同时已经创建的线程数小于最大线程数的话,那么会创建新的线程来处理阻塞队列中的任务。
- keepAliveTime: 线程没有任务处理时,还可以存活多久。(和unit参数配合生效,unit是单位,keepAliveTime是数值)。
- unit: 时间单位,比如秒,分等
- workQueue: 阻塞队列;用于存储等待执行的任务,有四种阻塞队列类型,ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
- threadFactory: 线程工厂,用来创建线程,主要是为了给线程起名字。
- handler: 线程池达到最大线程数之后,再来任务,由这个handler来执行具体拒绝策略。
默认提供的四种线程池:
- CachedThreadPool: 缓存线程池,该类线程池中线程的数量是不确定的,理论上可以达到Integer.MAX_VALUE个,这种线程池中的线程都是非核心线程,既然是非核心线程,那么就存在超时淘汰机制了,当里面的某个线程空闲时间超过了设定的超时时间的话,就会回收掉该线程;
- FixedThreadPool: 固定线程池,这类线程池中是只存在核心线程的,对于核心线程来说,如果我们不设置allowCoreThreadTimeOut属性的话是不存在超时淘汰机制的,这类线程池中的corePoolSize的大小是等于maximumPoolSize大小的,也就是说,如果线程池中的线程都处于活动状态的话,如果有新任务到来,他是不会开辟新的工作线程来处理这些任务的,只能将这些任务放到阻塞队列里面进行等到,直到有核心线程空闲为止;
- ScheduledThreadPool: 任务线程池,这种线程池中核心线程的数量是固定的,而对于非核心线程的数量是不限制的,同时对于非核心线程是存在超时淘汰机制的,主要适用于执行定时任务或者周期性任务的场景;
- SingleThreadPool: 单一线程池,线程池里面只有一个线程,同时也不存在非核心线程,感觉像是FixedThreadPool的特殊版本,他主要用于确保任务在同一线程中的顺序执行,有点类似于进行同步吧;
创建出线程池后,就是我们 excute() 方法,将我们的任务添加到线程池中:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果线程池当前线程数小于核心线程数,则创建新线程处理。
if (workerCountOf(c) < corePoolSize) {
//通过addWorker方法创建一个新的Worker对象来执行我们当前的任务;
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果核心线程都满了,则尝试把新来的任务放到阻塞队列里
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//如果线程池不在Running状态的话,会将刚刚添加到阻塞队列中的任务移出,同时拒绝当前任务请求
if (! isRunning(recheck) && remove(command))
reject(command);
//当前线程池中的工作线程数量是否为0,如果为0的话,就会通过addWorker方法创建一个Worker对象出来处理阻塞队列中的任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//放入阻塞队列失败(队列满了),就再创建新线程进行处理,这里addWorker第二个参数为false,表示非核心线程
else if (!addWorker(command, false))
reject(command);//阻塞队列满了,线程数也达到最大了,则执行拒绝策略
}
当达到最大线程数且阻塞队列满了时,再添加任务就会执行拒绝策略,主要有四种都在 ThreadPoolExcutor 中定义的静态内部类:
- CallerRunsPolicy: 直接使用调用该execute的线程本身来执行
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- AbortPolicy: 直接抛出RejectedExecutionException异常,丢弃任务
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- DiscardPolicy: 丢弃任务,但是不会抛出异常
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy: 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
为任务分配线程后,线程池又是如何实现线程的复用呢?
默认情况下,核心线程不会销毁,会一直尝试从阻塞队列里取任务去执行,除非使用 allowCoreThreadTimeOut 设置超时淘汰;
而非核心线程如果没有任务要处理,会在60s之后销毁。而如果在60s之内从阻塞队列里取到任务了,就会继续执行任务。
线程池怎么取任务?在我们提交一个任务时,调用 ThreadPoolExecutor 的 execute() 方法-> addWorker() -> addWorker() 里的 t.start() -> Worker的run()方法 -> runWorker(),而 runWorker 中 while 循环的条件中的 getTask()就是从阻塞队列中获取任务并进行处理的:
getTask(),从 workQueue 中获取:
最后就是线程的关闭了,涉及两个方法:
- shutdown()
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
- shutdownNow()
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
advanceRunState 方法设置线程池的状态,有四种状态:
对应四个状态:
- RUNNING:可以接受新任务,同时也可以处理阻塞队列里面的任务
- SHUTDOWN:不可以接受新任务,但是可以处理阻塞队列里面的任务
- STOP:不可以接受新任务,也不处理阻塞队列里面的任务,同时还中断正在处理的任务
- TIDYING:属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated方法
- TERMINATED:终止状态,这个状态是在调用完terminated方法之后所处的状态
shutdown中的interruptIdleWorkers:
shutdownNow中的interruptWorkers:
可以看到都是调用 interrupt() 方法,主要区别是:在 shutdown 的判断条件中,先尝试获取锁:
而一个任务在执行时是,上锁的状态,所以 shutdown 不会关闭还在执行的任务。
区别:
- shutdown():线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池
- shutdownNow():线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行