往期推荐
一、为什么要有线程池
在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:
1. 降低资源消耗
线程池可以充分复用线程,让线程持续不断地处理任务,有效避免频繁创建和销毁线程造成的资源消耗。
2. 提高响应速度
当任务到达时,任务可以不需要等待线程的创建就立即执行,没有线程创建和销毁时的消耗。
3. 提高线程的可管理性
可以避免无限创建线程引起`OutOfMemoryError`,如果无限制地创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同意分配、调优和监控。
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 线程池的核心工作流程:
二、创建线程池的七种方法
- (1)Executors.newFixedThreadPool:
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;使用了LinkedBlockingQueue队列,该队列其实是有界队列。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- (2)Executors.newCachedThreadPool:
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;它可容纳的最大线程数量为Integer.MAX_VALUE,所以比较容易出现内存溢出。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- (3)Executors.newSingleThreadExecutor:
创建单个线程数的线程池,它可以保证先进先出的执行顺序;它只会用一个唯一的工作线程来执行任务,如果这个唯一线程因为异常结束,那么会有一个新的工作线程来替代它,它必须保证前一项任务执行后,才能执行后一项任务。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- (4)Executors.newScheduledThreadPool:
创建一个可以执行延迟任务的线程池;可安排在给定延迟后运行命令或者定期地执行。使用了DelayedWorkQueue队列,该队列具有延时的功能
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
}
- (5)Executors.newSingleThreadScheduledExecutor:
创建一个单线程的可以执行延迟任务的线程池;
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
- (6)Executors.newWorkStealingPool:
创建一个抢占式执行的线程池(JDK1.8新增的新线程池)会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
- (7)ThreadPoolExecutor:
最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。
三、ThreadPoolExecutor详解
1、ThreadPoolExecutor的七个参数
在阿里巴巴的开发规约中明确指出,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor去创建,因为这样的处理方式可以让我们更加明确线程池的运行规则,规避资源耗尽的风险。
public ThreadPoolExecutor(int corePoolSize, //核心线程池的大小
int maximumPoolSize, //最大的核心线程池的数量
long keepAliveTime, //没人调用的存活时间,即最大空闲时间
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, // 线程工厂,用来创建线程的
RejectedExecutionHandler handler //拒绝策略
1. int corePoolSize:线程池内的核心线程的数量。如果属性allowCoreThreadTimeOut为false ,那么核心线程即使处于空闲状态也不会被回收。如果属性为true,那么核心线程也可以被销毁。
2. int maximumPoolSize:线程池可容纳的最大线程数量。
3.long keepAliveTime:空闲线程等待新任务的最大等待时间。
4.TimeUnit unit:keepAliveTime的时间单位。
5.BlockingQueue<Runnable> workQueue:在任务被执行之前用于保存任务的队列。
6.ThreadFactory threadFactory:线程池创建新线程时使用的线程工厂。
7.RejectedExecutionHandler handler:线程池的拒绝执行处理程序。(4种拒绝策略)
2、ThreadPoolExecutor的四种拒绝策略
- AbortPolicy:当线程池饱和后,抛出RejectedExecutionException异常。
/**
* 拒绝策略1 AbortPolicy ->超出最大承载量抛java.util.concurrent.RejectedExecutionException
*/
public class demo1 {
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
0,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try{
//最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
for (int i = 1; i <=9; i++) {
//超过maximumPoolSize+capacity的大小,会被拒绝并抛出java.util.concurrent.RejectedExecutionException异常
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
- CallerRunsPolicy:当线程池饱和后,由提交任务的线程执行任务。
/**
* 拒绝策略2 CallerRunsPolicy ->超出最大承载量时返回主线程进行处理
*/
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
0,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
try{
//最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
for (int i = 1; i <=9; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
- DiscardPolicy:默默地丢弃任务。
/**
* 拒绝策略3 DiscardPolicy ->超出最大承载量时丢弃任务,不抛出异常
*/
public class demo3 {
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
0,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
try{
//最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
for (int i = 1; i <=9; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
- DiscardOldestPolicy:丢弃阻塞队列中最早入队的任务。
/**
* 拒绝策略4 DiscardOldestPolicy ->超出最大承载量时丢弃队列里最久未处理的请求,然后重试,不会抛出异常
*/
public class demo4 {
public static void main(String[] args) {
//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2,
5,
0,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
try{
//最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
for (int i = 1; i <=9; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
3、ThreadPoolExecutor的工作流程
-
判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
-
判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
-
判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。
工作流程如图:
- 当提交任务到线程池执行时,线程池的状态变化如下: (1)当线程池内的线程数量少于corePoolSize时,线程池创建新线程用于执行提交到线程池的任务。
(2)当线程池内的线程数量大于等于corePoolSize时,任务将进入阻塞队列,等待核心线程执行。如果阻塞队列没有达到最大容量,那么任务入队成功,否则执行步骤 3。
(3)当线程池内的线程数量大于等于corePoolSize并且阻塞队列已满时,线程池内就会创建新的非核心线程用于执行任务。
(4)如果线程池内的数量小于maximumPoolSize,那么创建非核心线程成功,否则执行步骤 4。
(5)当线程池中的线程数量等于maximumPoolSize时,线程池拒绝执行处理程序。
4、ThreadPoolExecutor的五种状态
-
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。 -
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。 -
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。 -
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。 -
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。进入TERMINATED的条件如下:-
线程池不是RUNNING状态;
-
线程池状态不是TIDYING状态或TERMINATED状态;
-
如果线程池状态是SHUTDOWN并且workerQueue为空;
-
workerCount为0;
-
设置TIDYING状态成功。
-
5、关闭ThreadPoolExecutor
- 关闭线程池,可以通过shutdown和shutdownNow两个方法。
-
shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表; -
shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
-
6、合理配置ThreadPoolExecutor队列大小
-
CPU密集型任务- 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
-
IO密集型任务- 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
-
混合型任务- 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
参考 & 鸣谢
《Java并发编程的艺术》