由于系统资源是有限的,为了降低资源消耗,提高系统的性能和稳定性,引入了线程池对线程进行统一的管理和监控,本文将详细讲解线程池的使用、原理。
一、为什么使用线程池
(一) 池化思想
线程池主要用到了池化思想,池化思想在计算机领域十分常见,主要用于减少资源浪费、提高性能等。
池化思想主要包含以下几个方面:
资源池 | 池化思想的核心,表示一组可重复使用的资源,如数据库连接池、线程池。池中的资源可以被多个任务或线程共享,并且可以通过请求和释放的方式来管理。 |
资源重用 | 鼓励资源重复使用,而不是每次需要都创建新的资源实例。 |
资源管理 | 要求有效控制资源的管理和分配,防止资源浪费、耗尽。一般包含资源的申请、分配、使用、回收和释放等操作。 |
资源限制 | 池化还可以用于限制系统对某些资源的使用,以防止资源滥用或过度消耗。如,线程池可以限制系统中同时运行的线程数量,避免线程过多导致系统负载过重。 |
资源释放 | 池化确保在资源不再使用时,它们被释放和返回到资源池,以供后续的使用。这有助于避免资源泄漏和资源消耗问题。 |
一些常见的资源池包括线程池、数据库连接池、对象池、缓存池、连接池等。
池化思想可以提高系统的性能,因为它减少了资源的创建和销毁次数,避免了不必要的开销。通过池化,系统可以更好地应对高并发情况,降低资源竞争,提高响应速度。
(二) 什么是线程池
根据池化思想,在一个系统中,为了避免线程频繁的创建和销毁,让线程可以复用,引入了线程池的概念。线程池中,总有那么几个活跃线程。
当你需要使用线程时,可以从池子中随便拿一个空闲线程,当完成工作时,并不急着关闭线程,而是将这个线程退回到池子,方便其他人使用。
简单说就是,在使用线程池后,创建线程变成了从线程池中获得空闲线程,关闭线程编程了向池子里归还线程。
大致流程如下:
(三) 为什么使用线程池
Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
在开发过程中,合理地使用线程池能够带来3个好处。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
要做到合理利用线程池,必须对其实现原理了如指掌。
二、线程池的使用
(一) ThreadPoolExecutor
ThreadPoolExecutor 的创建方法总体来说可分为 2 种:
- 通过 ThreadPoolExecutor 构造函数
- 通过 Executors 类创建
1. 通过构造函数
1.1. 入参含义
这个也是推荐使用的方法,因为通过 Executors 类创建可能会导致 OOM,如下图阿里开发规范中的描述。
构造函数入参:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
构造函数入参含义:
参数名称 | 含义 |
corePoolSize | 核心线程数,线程池中始终存活的线程数。 |
maximumPoolSize | 最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。 |
keepAliveTime | 当线程池中的线程数超过 corePoolSize 时,超过的空闲线程存活时间,且时间到达将被销毁。 |
unit | keepAliveTime 的时间单位,最大到天,最小到纳秒。 |
workQueue | 阻塞队列,用来存储线程池等待执行的任务,均为线程安全。 |
threadFactory | 线程工程,用于创建线程,一般默认即可 |
handler | 拒绝策略,当任务过多无法处理时,如何拒绝。 |
1.2. 阻塞队列
workQueue 可选的 BlockingQueue:
分类 | 说明 |
直接提交队列 | 使用 SynchronousQueue,一个不存储元素的阻塞队列,每一个插入操作都要等待一个相应的删除操作,反之亦然。 该队列提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果线程数量己经达到最大值,则执行拒绝策略。 因此,使用该队列,通常要设置很大的 maximumPoolSize 值,否则很容易执行拒绝策略。 |
有界任务队列 | 可使用 ArrayBlockingQueue,构造函数的入参表示最大容量。只有在队列达到最大容量时,线程池的线程数才会大于 corePoolSize,一般建议线程数维持在 corePoolSize。 |
无界任务队列 | 可使用 LinkedBlockingQueue,超过 corePoolSize 的任务都会加入到无界队列中,若任务创建和处理的速度差异很大,无界队列会快速堆积,耗尽系统内存。 |
优先任务队列 | 可使用 PriorityBlockingQueue,可控制任务执行顺序,特殊的无界队列,根据任务的优先级来执行,其他的一般都是按照先进先出的顺序。 |
1.3. 拒绝策略
策略 | 说明 |
AbortPolicy | 拒绝并直接抛出异常。(默认) |
CallerRunsPolicy | 只要线程池未关闭,该策略直接在当前调用的线程中,运行被丢弃的任务。这样做不是真正的丢弃任务,且任务提交线程的性能可能急剧下降。 |
DiscardOldestPolicy | 丢弃最老的一个请求,也就是即将被执行的、队列头部的任务,并尝试执行。 |
DiscardPolicy | 丢弃当前任务,不予任何处理,允许任务丢失的场景下可选。 |
如下图,上述拒绝策略均实现 RejectedExecutionHandler 接口,且为 ThreadPoolExecutor 的内部类。
若以上策略仍无法满足实际应用需要,完全可以自已扩展 RejectedExecutionHandler 接口。
public interface RejectedExecutionHandler {
/**
* @param r 当前请求执行的任务
* @param executor 当前的线程池
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
示例:
public class RejectedExecutionDemo {
public static class MyTask implements Runnable{
@Override
public void run() {
System.out.println(new Date() + ":Thread ID is" + Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask myTask = new MyTask();
ExecutorService executorService = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
(r, executor) -> System.out.println(r.hashCode() + "is discard")
);
for (int i = 0; i < 100; i++) {
executorService.submit(myTask);
Thread.sleep(10);
}
}
}
上述示例中,mytask 执行需要花费100毫秒,因此,必然会导致一些任务被直接丢弃。在实际应用中,我们可以将更详细的信息记录到日志中,来分析任务丢失情况和系统负载。
2. 通过 Executors
Executors 类扮演着线程池工厂的角色,通过该类可以取得一个拥有定功能的线程池。
该类可以创建三种类型的 ThreadPoolExecutor:
- FixedThreadPool
- SingleThreadExecutor
- CachedThreadPool
2.1. FixedThreadPool
固定线程数的线程池,该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂时存在任务队列中,待有线程空闲时,在处理队列中的任务。
FixedThreadPool 使用的无界任务队列 LinkedBlockingQueue,可能造成内存泄露。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
2.2. SingleThreadExecutor
只有一个工作线程的线程池,当多于 1 个任务被提交时,会存到任务队列中。该线程池使用的无界任务队列 LinkedBlockingQueue,可能造成内存泄露。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
2.3. CachedThreadPool
根据实际情况调整线程数的线程池,线程池的线程数量不确定,若有空闲线程可复用,则会优先使用。若所有线程均在工作,此时新的任务则会创建新的线程优先处理。所有线程在任务执行完毕后,将返回线程池进行复用。
corePoolSize 被设置为0,maximumPoolSize 被设置为无界,存活时间设置为 60s,空闲线程超过60秒后将会被终止。极端情况线程创建过多,会导致内存泄露。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
(二) ScheduledThreadPoolExecutor
1. 简介
如下图, ScheduledThreadPoolExecutor 继承自ThreadPoolExecutor,它主要用来定期执行任务,功能与 Timer 类似且更加强大,可以在构造函数中指定多个对应的后台线程数。
2. 使用
可通过 Executors 创建,源码如下:
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
这里的返回值是 ScheduledExecutorService,根据时间对线程进行调度。有三个主要方法:
public interface ScheduledExecutorService extends ExecutorService {
/**
* 给定时间对任务进行调度
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 周期性对任务进行调度
* 以第一个任务的开始时间 initialDelay + period
* 第一个任务在 initialDelay + period 执行
* 第二个任务在 initialDelay + period * 2 执行
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 周期性对任务进行调度
* 上一个任务结束后,再经过 period 时间开始执行
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
如果任务遇到异常,那么后续的所有子任务都会停止调度,因此,必须保证异常被及时处理,为周期性任务的稳定调度提供条件。
(三) ForkJoinPool
fork 是开启子进程,join 是等待,意思是分支子进程结束后才能得到结果,实际开发中,若频繁的 fork 开启线程可能严重影响系统性能,所以引入了 ForkJoinPool。
大致流程是,向 ForkJoinPool 线程池中提交一个 ForkJoinTask 任务,就是将任务分解成多个小任务,等任务全部完成后进行处理,这里采用了分治的思想,具体我将在后续单独展开,这里不多做赘述。
ForkJoin 可能出现两个问题:
- 子线程积累过多,可能导致系统性能严重下降;
- 调用层次过深,可能导致栈溢出。
三、线程池的任务提交
(一) execute()
该方法用于提交不需要返回值的任务,且无法判断任务是否被线程池执行成功。
源码见下面的线程池原理章节。
(二) submit()
该方法用于提交需要返回值的任务。线程池会返回 Future 对象,可以判断任务是否执行成功,还可以通过 Future 的get()方法来获取返回值。
get()方法会阻塞当前线程直到任务完成,还可以设置超时时间,到时立即返回,不过这时有可能任务没有执行完。
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
四、线程池的关闭
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt() 来中断线程,所以无法响应中断的任务可能永远无法终止。
两种方法存在一定的区别,shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,表示线程池关闭成功,这时调用isTerminaed方法会返回true。
至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。
五、线程池执行原理
(一) 执行源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果当前工作线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 添加核心线程去执行任务,成功则return
if (addWorker(command, true))
return;
// 添加失败,ctl有变化,需重新获取
c = ctl.get();
}
// 判断是否为RUNNING,此时核心线程数已满,需加入任务队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 检查若不是RUNNING则将任务从队列移除
if (! isRunning(recheck) && remove(command))
// 执行拒绝策略
reject(command);
// 正常则添加一个非核心空线程,执行队列中的任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 表示核心线程满了,队列也满了,创建非核心线程,执行任务
else if (!addWorker(command, false))
// 最大线程数也满了,走拒绝策略
reject(command);
}
(二) 流程图
参考:
[1] 魏鹏. Java并发编程的艺术.
[2] 葛一鸣/郭超. 实战Java高并发程序设计.