合理地使用线程池能够带来3个好处:
- 降低资源消耗(通过重复利用已创建的线程降低线程创建和销毁造成的消耗)
- 提高响应速度(当任务到达时,任务可以不需要等到线程创建就能立即执行)
- 提高线程的可管理性(线程池可以进行统一分配、调优和监控线程)
1.线程池的实现原理
当提交一个新任务到线程池时,线程池的处理流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务:
- 不是则创建一个新的工作线程来执行任务
- 都在执行任务则进入下个流程
- 线程池判断工作队列是否已经满:
- 没满则将新提交的任务存储在这个工作队列里
- 满了则进入下个流程
- 线程池判断线程池的线程是否都处于工作状态:
- 没有则创建一个新的工作线程来执行任务
- 满了则交给饱和策略来处理这个任务
ThreadPoolExecutor执行execute方法分下面4种情况:
- 如果当前运行的线程少于
corePoolSize
,则创建新线程来执行任务(需要获取全局锁) - 如果运行的线程等于或多于
corePoolSize
,则将任务加入BlockingQueue
- 如果
BlockingQueue
队列已满,则创建新的线程(非核心线程)来处理任务(需要获取全局锁) - 如果创建新线程将使当前运行的线程超出
maximumPoolSize
,任务将被拒绝,并调用
RejectedExecutionHandler.rejectedExecution()
方法
什么是工作线程?线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行
2.线程池的使用
2.1 线程池的创建
通过
new ThreadPoolExecutor(xxx)
,其中参数具体含义如下:
corePoolSize
(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务(即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建,allowCoreThreadTimeOut
方法可设置核心线程是否能被回收)runnableTaskQueue
(任务队列):用于保存等待执行的任务的阻塞队列(可选择Java并发容器和框架中2.2节介绍的队列)maximumPoolSize
(线程池最大数量):线程池允许创建的最大线程数(如果队列满了且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务)ThreadFactory
:设置创建线程的工厂RejectedExecutionHandler
(饱和策略):当队列和线程池都满了,则采取一种策略处理提交的新任务AbortPolicy
(默认):直接抛出异常。CallerRunsPolicy
:只用调用者所在线程来运行任务。DiscardOldestPolicy
:丢弃队列里最近的一个任务,并执行当前任务DiscardPolicy
:不处理,丢弃掉且不抛出异常
keepAliveTime
(线程活动保持时间):线程池的工作线程空闲后保持存活的时间(超过该时长,非核心线程就会被回收)TimeUnit
(线程活动保持时间的单位)
2.2 向线程池提交任务
可以使用两个方法向线程池提交任务:execute()和submit()
execute()
:用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
threadsPool.execute(new Runnable() {
@Override
public void run() {
...
}
})
submit()
:用于提交需要返回值的任务(返回future
类型的对象,该对象可以判断任务是否执行成功且可以通过对象的get()
来获取返回值)
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}
2.3 关闭线程池
可通过调用线程池的
shutdown
或shutdownNow
方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程
2.4 合理地配置线程池
可以从以下几个角度配置线程池:
- 任务的性质:
- CPU密集型任务:配置尽可能小的线程(
Ncpu+1
),因为CPU密集型任务使得CPU使用率很高,若开过多的线程数能增加上下文切换的次数,带来额外的开销 - IO密集型任务:配置尽可能多的线程(
2*Ncpu
),因为CPU使用率并不高,可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间 - 混合型任务:可将任务分成IO密集型和CPU密集型任务(两个任务执行时间相差不大时),然后分别用不同的线程池去处理
- CPU密集型任务:配置尽可能小的线程(
- 任务的优先级:优先级不同的任务可以使用优先级队列
PriorityBlockingQueue
来处理 - 任务的执行时间:可以交给不同规模的线程池来处理
- 任务的依赖性:比如依赖数据库连接池的任务,线程提交SQL后需要等待数据库返回结果,等待的时间越长,CPU空闲时间就越长(相当于IO密集型任务,应设置较大线程数)
2.5 线程池的监控
如果在系统中大量使用线程池,则需要对线程池进行监控,在出现问题时,可根据线程池的使用状况快速定位问题。可使用以下属性:
taskCount
:线程池需要执行的任务数量completedTaskCount
:线程池在运行过程中已完成的任务数量largestPoolSize
:线程池里曾经创建过的最大线程数量getPoolSize
:线程池的线程数量(线线程池不销毁的话,线程池里的线程不会自动销毁)getActiveCount
:获取活动的线程数- 重写线程池的
beforeExecute
、afterExecute
和terminated
方法进行监控
3.已有线程池
-
定长线程池(
FixedThreadPool
):用于控制线程最大并发数- 只有核心线程
- 线程数量固定
- 执行完立即回收
- 任务队列为链表结构的有界队列(消耗内存)
-
定时线程池(
ScheduledThreadPool
):用于执行定时或周期性的任务- 核心线程数量固定
- 非核心线程数量无限(线程过多导致内存溢出)
- 执行完闲置10ms后回收
- 任务队列为延时阻塞队列
-
可缓存线程池(
CachedThreadPool
):执行大量且耗时少的任务- 无核心线程
- 非核心线程数量无限(线程过多导致内存溢出)
- 执行完闲置60s后回收
- 任务队列为不存储元素的阻塞队列
-
单线程化线程池(
SingleThreadExecutor
):应用于不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作- 只有1个核心线程
- 无非核心线程
- 执行完立即回收
- 任务队列为链表结构的有界队列(消耗内存)