深入理解Java高并发编程(6) - 线程池
1. 线程池
每次为新来的任务创建线程是非常低效的,应该通过线程池的方式提供线程执行任务,做到线程的共用。
大概的模式是这样的,线程池是消费者,每次从任务队列去拿任务执行,生产者是主线程,每次产生新的任务放在任务队列中,当线程池中线程数量不能再新增,且任务队列满的时候,这时候main不能再往blocking queue里放任务,而是执行拒绝策略。

2. ThreadPoolExecutor

- ExecutorService:线程池最基本的接口,定义了提交任务,关闭线程池等基本方法
- ThreadPoolThread:jdk提供的线程池最基本实现,使用int的高3位表示线程池状态,低29位表示线程数量。
- 为什么把线程池状态和线程数量放在一个整数中:这样做的好处是只需要做一次CAS操作修改这两个值。
ThreadPoolExecutor构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目,减去核心线程数就是可创建的救急线程数目
- 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时命名
- handler 拒绝策略
线程池中包括两种线程,两种线程开启都是懒惰创建
- 核心线程:先创建核心线程,核心线程处理新来的任务,当核心线程正在处理任务,新来的任务会被放到任务队列(阻塞队列)中。
- 救急线程:当核心线程达到最大核心线程数目,且任务队列满了,创建救急线程来执行任务。救急线程和核心线程最大区别是,救急线程有生存时间,当任务执行完了,经过生存时间后救急线程会被回收。 当救急线程和核心线程达到了线程池最大数目,且阻塞队列满了,最后执行拒绝策略。 无界阻塞队列不会触发救急线程创建和拒绝策略。
3. 拒绝策略
当任务队列满(只有有界队列才会存在拒绝策略),且线程池达到最大线程数(救急线程 + 核心线程都达到最大)时,线程池无法接受新的任务,此时执行拒绝策略
常见拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException,拒绝任务;CallerRunsPolicy:由提交任务的线程(如主线程)自己执行任务,降低并发压力;DiscardPolicy:静默丢弃新任务,不抛异常;DiscardOldestPolicy:丢弃队列中最旧的任务,尝试将新任务 加入队列。

4. 线程池状态 & 关闭线程池
| 状态名 | 高 3 位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
|---|---|---|---|---|
| RUNNING | 111 | Y | Y | |
| SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
| STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
| TERMINATED | 011 | - | - | 终结状态 |
- shutdown():
- 线程池状态变为shutdown
- 不会接受新任务
- 已经在队列中的任务会被执行完
- 方法不会阻塞调用线程的执行
- shutdownNow():
- 线程池状态变为stop
- 不会接受新任务
- 会把队列中任务返回
- 会用interrupt的方式中断正在执行的任务。
5. 线程池工厂方法
Executor类下有许多创建线程池的工厂方法
- newFixedThreadPool
- 只有核心线程,没有救急线程,阻塞队列使用的LinkedBolckingQueue,属于无界队列,也不涉及拒绝策略
- 适合任务量已知,耗时的任务。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
newCachedThreadPool
- 核心线程数为0,创建的都是救急线程,闲置60s后回收,救急线程可以无限创建
- 队列采用了SynchronousQueue,无界队列,没有线程来取是放不进去的。(放取同步)
- 适合任务数比较密集,每个任务执行时间较短的场景。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
newSingleThreadExecutor
- 单线程线程池,只有一个核心线程。
- 区分单独创建一个线程执行任务
- 单线程线程池保证永远有一个线程在工作,即使之前的工作线程因为异常停止,也会创建新的线程执行任务,而单独创建线程执行任务,异常了就被回收了
- 和newFixedThreadPool(1)区别
- Executor.newSingleThreadExecutor采用装饰器模式,对外只暴露ExecutorService接口,因此不能调用ThreadPoolExecutor中的方法。
- newFixedThreadPool还可以修改最大核心线程数
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new AutoShutdownDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
6. 线程池里面的execute 和 submit 方法有什么区别 & 为什么线程池可以执行callable而new thread要执行FutureTask
-
execute和submit都可以用来执行方法
-
execute用来执行runnable的方法,如果方法中出现了异常,线程会终止,异常会被传递给线程池的
UncaughtExceptionHandler,需要手动在任务内部捕获异常,否则异常会丢失。 -
submit既可以执行runnable的方法(执行Runnable方法时会被封装成Callable
),也可以执行带返回值的callable方法 ,当出现异常时,通过Future.get去获取返回结果会打印报错信息。
-
-
之前创建线程的时候,为什么Thread直接接受runnable的变量,如果使用callable需要先封装成futureTask。为什么线程池可以直接submit执行callable呢?
- 线程池并不是 “直接执行 Callable”,而是帮你自动完成了 FutureTask 的创建和适配—— 你看到的
submit(Callable)是线程池提供的 “语法糖”,底层和Thread + FutureTask的逻辑完全一致,只是省去了手动包装的步骤。
- 线程池并不是 “直接执行 Callable”,而是帮你自动完成了 FutureTask 的创建和适配—— 你看到的
7. Timer & 任务调度线程池 ScheduledThreadPool
Timer(已过时):在任务调度线程池加入之前,使用Timer定时执行任务。
不使用runnable 或 callable,而是接受TimerTask执行任务,出现异常未被捕捉的时候,Timer会无法执行剩下要执行的任务。
SchuduledThreadPool,可以做到延时执行任务(延迟一段时间开始执行任务)和定时执行任务(隔一段时间执行一次)。
- 通过Executor.newScheduledThreadPool工厂方法创建
- 出现异常的时候,也不会影响接下来的任务运行。
8. ForkJoinThread 和 RecursiveTask
任务可以被拆分成子任务
RecursiveTask用于有返回结果的任务拆分,RecursiveAction是没有返回值的任务拆分。
通过实现在compute方法中实现任务拆分的逻辑,recursive中的fork方法是通过让一个线程去执行,join方法是获取任务的结果,通过这个两个方法来实现任务拆分逻辑。
ForkJoinPool可以去执行被拆分的子任务(RecursiveTask/RecursiveAction),使用invoke方法来执行一个rucursive的任务,ForkJoinPool通过工作窃取算法高效处理子任务
工作窃取:
- 每个工作线程都有自己的任务队列(双端队列);
- 线程优先执行自己队列里的任务;
- 当自己的队列空了,会去其他线程的队列偷取任务(通常偷取队列尾部的任务);
- 优势:减少线程间竞争,最大化利用空闲线程,提升并行效率。

浙公网安备 33010602011771号