1.什么是线程池
线程池就是提前创建若干个线程,若有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。减少频繁创建和销毁线程消耗系统资源。
2.为什么要用线程池
频繁创建、销毁 线程。会对系统资源的极大浪费。如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性。因此,实际开发会使用线程池来管理、复用线程。
3.使用线程池的优点
- 降低资源消耗: 重复利用线程,减少创建和销毁造成的消耗。
- 提升响应速度: 任务到达,不需要创建,立即执行。
- 提高可管理型: 线程是CPU调度和分派的基本单位,对任务统一进行 分配、调优和监控。
4.创建线程池的方式
Java从1.5 Executors 类提供四种创建线程池方式
4.1newSingleThreadExecutor
- 单个线程的线程池,每次只有一个线程工作,单线程串行执行任务
- 队列长度-Integer.MAX_VALUE
4.2newFixedThreadPool
- 定线程数,线程池 ,核心线程数=最大线程数=设置的线程数,可控制线程最大并发数
- 队列长度-Integer.MAX_VALUE
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),threadFactory);
}
4.3newCachedThreadPool
- 可缓存线程池,有任务才新建线程,闲置线程保存60秒
- 最大线程数长度-Integer.MAX_VALUE
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
4.4newScheduledThreadPool
- 定核心线程数,线程池,支持定时及周期性任务执行。
- 最大线程数长度-Integer.MAX_VALUE
5.为什么不建议使用 Executors静态工厂构建线程池
主要原因:队列堆积,有OOM风险
5.1.FixedThreadPool 和 SingleThreadPool
允许的请求队列(底层实现是LinkedBlockingQueue)队列长度为Integer.MAX_VALUE,
可能会堆积大量的请求,从而导致OOM
5.2.CachedThreadPool 和 ScheduledThreadPool
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
6.线程池参数与含义
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
参数 | 含义 |
---|---|
corePoolSize | 核心线程数量,一直存在 除非allowCoreThreadTimeOut设置为true |
maximumPoolSize | 线程池允许的最大线程池数量 |
keepAliveTime | 线程数量超过corePoolSize, 空闲线程的最大超时时间 |
unit | 超时时间的单位 |
workQueue | 工作队列,保存等待执行任务的阻塞队列 |
threadFactory | 自定义线程的工厂类,一般用来设置线程名称 |
handler | 当线程池和队列都满了,再加入线程会执行此策略 详见第8点 |
附1.时间参数取值范围
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
附2.常见阻塞队列
- ArrayBlockingQueue
- 是一个基于数组结构的有界阻塞队列,按 FIFO(先进先出)原则对元素进行排序,创建时必须指定大小;
- LinkedBlockingQueue
- 基于链表的先进先出队列,若创建时没有指定队列大小,则默认为Integer.MAX_VALUE;
- synchronousQueue
- 是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列
综上,一般建议使用LinkedBlockingQueue,并合理设置队列大小
7.线程池的执行顺序
8.拒绝策略
拒绝策略也可以叫饱和策略,作为线程池定义的最后一个参数,同样至关重要。线程数和队列不可能无穷大,当队列和线程都满了的时候,那么必须采取一种策略去处理继续追加的任务。于是就有了拒绝策略的引入。<br />以下是内置的4中拒绝策略,默认是ThreadPoolExecutor.AbortPolicy,当超过最大值时直接抛出异常
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常 |
---|---|
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃队列最前面的任务,然后重新提交被拒绝的任务 |
ThreadPoolExecutor.CallerRunsPolicy | 由调用线程(提交任务的线程)处理该任务 |
9.如何合理设置线程池大小
《Java并发编程实战》中最原始的公式是这样的:
N = N * U * (1 + W/C)
其中 _N_是_CPU_核心数 , _U_是_CPU_使用率介于0~1之间 , W/C 是等待时间与计算时间的比率
java可以通过如下代码获取当前设备的CPU个数。
Runtime.getRuntime().availableProcessors()
实际使用中一般会根据任务类型划分,对于不同类型的任务适当分配不同大小的线程池
- CPU密集型任务
- 尽量使用较小的线程池,一般为CPU核心数+1。
- 因为CPU密集型任务使得CPU使用率很高,若开过多的线程,会增加上下文切换的次数,带来额外的开销。
- IO密集型任务
- 可以使用稍大的线程池,一般为2*CPU核心数。
- IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
- 混合型任务
- 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 - 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
- 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
结论:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
10.线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭
- shutdown()
- 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,将不会接受新的任务
- 将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程
- shutdownNow()
- 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
- 遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程
- 先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
关注程序员小强公众号更多编程趣事,知识心得与您分享