一.使用线程池的好处
- 降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
二.线程池使用场景
1.用和不用有什么区别
- 不使用线程池的情况下,任务顺序执行
- 使用线程池的情况下,任务同时执行
2.什么时候适合用
- 对于多个耗时且互不依赖的任务,可选择使用线程池
eg. 我们可以做家务的时候可以同时拖地和洗衣服(洗衣机)
- 对于多个耗时且依赖结果的任务,可选择使用线程池+CountDownLatch
CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行(阻塞)
eg. 比如做一个项目需求,后端和前端同时开发(却不一定同时结束),这时候快的一方必须等到慢的一方完成还可上线
三.如何使用线程池
一般是通过ThreadPoolExecutor的构造函数来创建线程池,然后提交任务给线程池执行即可。
1.构造函数参数作用
参数 | 作用 |
---|---|
int corePoolSize | 线程池核心线程数 |
int maximumPoolSize | 线程池最大线程数 |
long keepAliveTime | 线程数大于核心线程数时,多余的空闲线程存活的最长时间 |
TimeUnit unit | 时间单位 |
ThreadFactory threadFactory | 线程工厂,用来创建线程,一般默认即可 |
RejectedExecutionHandler handler | 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 |
BlockingQueue |
阻塞队列,可指定大小 |
2.参数之间的关系
提交任务时:
- 如果
poolSize
<corePoolSize
,会新创建一条线程并执行该任务 - 如果
poolSize
=corePoolSize
,该任务会被放到阻塞队列(workQueue
)等待 - 如果
workQueue
满了,且poolSize
<maxmumPoolSize
,会新创建一条线程来执行该任务 - 如果
workQueue
满了,且poolSize
=maxmumPoolSize
,会根据拒绝策略handler
拒绝该任务
3.示例
new ThreadPoolExecutor().execute(()->{do sth.});
点击跳转
4.execute、submit区别
- 传参
- execute只能接受Runnable类型的任务
- submit能接受Runnable和Callable类型的任务
- 返回值
- execute没有返回值
- submit有返回值(通过Future.get()获取)
- 异常
- execute跟普通线程处理方式一致,通过try-catch捕获异常
- submit会在call()抛出异常,所以需要Future.get()才能抛出异常
5.建议不同类别的业务用不同的线程池
5.1 为什么呢?
一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务
5.2 错误案例
简单来说,就是线程池被父任务占满了,导致所有的子任务没有线程去执行(导致队列堆积),而且父任务会阻塞住,造成死锁
四.正确配置线程池参数
- 如果我们设置的线程池数量太小,如果同一时间有大量任务需要处理,那么就可能会导致大量任务要在任务队列等待,导致OOM。这样的CPU并没有得到充分利用。
- 如果我们设置的线程池数量太大,大量线程可能会同时争取CPU资源,导致有大量的上下文切换,从而增加线程的执行时间,影响整体的执行效率
所以我们在设置线程池大小时有个公式(N为CPU核心数)
- CPU密集型任务(N+1) 这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
- I/O密集型任务(2N+1) 这种任务引用起来,系统会用大部分的事件来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N+1。
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2 + 1); executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2 + 1); executor.setQueueCapacity(1000); executor.setKeepAliveSeconds(20); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
五.总结
- 建议不同场景使用不同的线程池,防止死锁
- 空间换时间
- 不允许使用Executors创建线程池,应该通过ThreadPoolExecutor去创建
- 不同线程池命不同的名(如pool-1-thread-n),有利于定位问题
六.拒绝策略
1.什么时候会触发拒绝策略?
当线程池的任务缓存队列workQueue
已满且线程大小poolSize
已达到maximumPoolSize
,如果这时候还有新任务到来就会采取任务拒绝策略,通常有以下四种策略
- AbortPolicy(默认):丢弃任务并抛出
RejectedExecutionException
异常 - DiscardPolicy:丢弃任务(不抛出异常)
- DiscardOldestPolicy:丢弃阻塞队列
workQueue
最前面的任务,然后执行新任务 - CallerRunPolicy:由调用线程执行该任务
2.AbortPolicy
创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。
public class AbortPolicyDemo {
private static final int THREAD_SIZE = 1;
private static final int CAPACITY = 1;
public static void main(String[] args) {
// 创建线程池:指定corePoolSize和maximumPoolSize都为1,阻塞队列大小为1
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_SIZE, THREAD_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(CAPACITY));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
for(int i=1;i<=10;i++) {
pool.execute(new MyRunnable("task-" + i));
}
}
public static class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//ignore
}
}
}
}
测试结果:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.garylee.config.server.controller.admin.AdminLeeObjectConfigController$MyRunnable@3f8f9dd6 rejected from java.util.concurrent.ThreadPoolExecutor@aec6354[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.garylee.config.server.controller.admin.AdminLeeObjectConfigController.main(AdminLeeObjectConfigController.java:150)
task-1 is running
task-2 is running
结果分析:
task-1
任务来了,起了一个线程,此时poolSize
=corePoolSize
=maxmumPoolSize
task-2
任务来了,发现poolSize
=corePoolSize
,就把task-2
塞入阻塞队列workQueue
中task-3
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发AbortPolicy
拒绝策略,直接抛出RejectExecutionException
异常task-1
执行完毕task-2
执行完毕- end
3.DiscardPolicy
创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。
/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
测试结果:
task-1 is running
task-2 is running
结果分析:
task-1
任务来了,起了一个线程,此时poolSize
=corePoolSize
=maxmumPoolSize
task-2
任务来了,发现poolSize
=corePoolSize
,就把task-2
塞入阻塞队列workQueue
中task-3
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardPolicy
拒绝策略,直接丢弃任务task-4
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardPolicy
拒绝策略,直接丢弃任务- …
task-10
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardPolicy
拒绝策略,直接丢弃任务task-1
执行完毕task-2
执行完毕- end
4.DiscardOldestPolicy
创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。
/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
测试结果:
task-1 is running
task-10 is running
结果分析:
task-1
任务来了,起了一个线程,此时poolSize
=corePoolSize
=maxmumPoolSize
task-2
任务来了,发现poolSize
=corePoolSize
,就把task-2
塞入阻塞队列workQueue
中task-3
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardOldestPolicy
拒绝策略,丢弃阻塞队列workQueue
最前面的任务task-2
task-4
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardOldestPolicy
拒绝策略,丢弃阻塞队列workQueue
最前面的任务task-3
- …
task-10
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发DiscardOldestPolicy
拒绝策略,丢弃阻塞队列workQueue
最前面的任务task-9
task-1
执行完毕task-10
执行完毕- end
5.CallerRunsPolicy
创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。
/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
测试结果:
task-1 is running
task-3 is running
task-2 is running
task-5 is running
task-4 is running
task-7 is running
task-6 is running
task-9 is running
task-8 is running
task-10 is running
结果分析:
task-1
任务来了,起了一个线程,此时poolSize
=corePoolSize
=maxmumPoolSize
task-2
任务来了,发现poolSize
=corePoolSize
,就把task-2
塞入阻塞队列workQueue
中task-3
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发CallerRunsPolicy
拒绝策略,加入task-1
所在线程执行task-1
和task-3
执行完毕,task-2
占用线程task-4
任务来了,发现poolSize
=corePoolSize
,就把task-4
塞入阻塞队列workQueue
中task-5
任务来了,发现poolSize
=corePoolSize
,且阻塞队列workQueue
满了,触发CallerRunsPolicy
拒绝策略,加入task-2
所在线程执行task-2
和task-5
执行完毕,task-4
占用线程- …
- end
七、线程、线程池状态
1、线程状态
线程有五种状态,分别为:
- 新建状态(New):表示新创建了一个线程对象
- 就绪状态(Runnable):线程对象创建后,如果其他线程调用该对象的start方法,该线程就会放在可运行线程池中,变得可运行,等待获取CPU的使用权
- 运行状态(Running):
Running
的线程获取了CPU,执行程序代码 - 阻塞状态(Blocked):线程由于某些原因放弃了CPU使用权,暂时停止运行,直到线程进入
Runnable
,才有机会转到Running
- 死亡状态(Dead):线程由于执行完毕或异常退出run方法,结束了生命周期
2、线程池状态
- 运行状态(RUNNING):表示线程池正常运行,能接受新任务,也能正常处理队列中的任务
- 关闭状态(SHUTDOWN):表示线程池处于关闭状态,不能接受新任务,能正常处理队列中任务
- 状态切换:调用shutdown()方法,线程池状态由
RUNNING
->SHUTDOWN
- 状态切换:调用shutdown()方法,线程池状态由
- 停止状态(STOP):表示线程池处于停止状态,不能接受新任务,不能正常处理队列中任务,正在运行的线程也会停止
- 状态切换:调用shutdownnow()方法,线程池状态由(
RUNNING
orSHUTDOWN
) ->STOP
- 状态切换:调用shutdownnow()方法,线程池状态由(
- 整理状态(TIDYING):线程池没线程在运行时,状态会自动变为
TIDYING
,并且会调用terminated()方法(该方法是空实现,留给调用方扩展)- 状态切换:当线程池在
SHUTDOWN
状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN
->TIDYING
。 当线程池在STOP
状态下,线程池中执行的任务为空时,就会由STOP
->TIDYING
- 状态切换:当线程池在
- 销毁状态(TERMINATED):表示线程池已彻底终止
- 状态切换:调用terminated()方法,线程池状态由
TIDYING
->TERMINATED
- 状态切换:调用terminated()方法,线程池状态由
八、一些问题
- 核心线程数可以设置为0吗?
- 非核心线程什么时候会销毁?销毁的判断依据是什么?:空闲的时候
- shutdown和shutdownnow的区别?