1. 为什么需要线程池
程序中不管是网络请求、文件IO、数据库操作等其他耗时操作都需要异步进行,而由于线程创建和销毁都需要一定的开销,如果每次执行异步任务都重新创建一个线程,并在完成任务后直接进行销毁,这会消耗大量资源。JAVA在1.5中提供了Executor,通过将任务的创建和执行解耦, 如下图所示 即通过Runnable和Callable接口实现延时启动/异步启动任务并通过Future返回执行结果 整个Executor最核心的就是ThreadPoolExecutor,我们首先来看看他的原理;
2. ThreadPoolExecutor
2.1 ThreadPoolExecutor的构造
ThreadPoolExecutor一共有四个构造方法,如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
可以看到最终都是调用了最后一个构造,其参数含义如下:
corePoolSize : 核心线程数,默认情况下新建的线程池是空的,当有新任务进来时,会判断当前线程数量是否小于核心线程数,小于则创建新线程执行该任务,反之不会创建,另外如果在创建线程池的同时调用了prestartAllcoreThread()则会在初始化完成后新建全部核心线程并等待任务; maximumPoolSize : 最大线程数,一个新任务进来时,如果当前任务队列已满但线程数还没有达到最大线程数,则创建新线程; keepAliveTime : 非核心线程闲置的超时回收时间,默认是1000ms,当非核心线程闲置超过这个时间后将会被回收,在一些任务数量多、任务平均耗时短的业务场景下可以适当调大此值以提高线程池效率,如果设置了allowCoreThreadTimeout(true)的话,则此超时时间限制也会用于核心线程; unit: keepAliveTime参数的单位,可以是天、时、分、秒、毫秒; workQueue: 任务队列,是一个阻塞队列; threadFactory: 线程工厂,可以用它来为线程池中的线程设置名称,用的场景不多,一般默认不传该参数即可; handler: 饱和策略,当线程池中任务队列已满且当前线程数已达到最大线程,如果此时有新任务进入,会触发饱和策略,默认为AbortPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。此外还有3种策略,它们分别如下: ①CallerRunsPolicy: 用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。 ②DiscardPolicy: 不能执行的任务,并将该任务删除。 ③DiscardOldestPolicy: 丢弃队列最近的任务,并执行当前的任务。
2.2 ThreadPoolExecutor中新任务处理流程
① 当一个新任务进入时,首先会判断当前线程数是否达到核心线程数,如果没达到则创建一个新的核心线程执行该任务; ② 如果当前未达到核心线程数,则创建核心线程执行任务,如已达到核心线程数,则检查当前任务队列是否已满; ③ 如果当前任务队列未满,则将新任务加到任务队列中,如任务队列已满,则判断当前线程数是否达到最大线程数; ④ 如果已经达到最大线程数,则触发饱和机制,如果未达到最大线程数,则创建一个新的线程执行新任务;
4.3 常用线程池
4.3.1 FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
通过构造方法可以很容易看出,FixedThreadPool特点:①核心线程数 = 最大线程数 = nThreads(构造传入);②keepAliveTime为0;③任务队列类型为一个无界阻塞队列。 结合前面对于ThreadPoolExecutor构造参数的解释,可以得出FixedThreadPool处理任务的过程如下: ①如果当前运行的线程数少于corePoolSize, 会立刻创建新线程执行任务。 ②当线程数到达corePoolSize后,将任务加入到LinkedBlockingQueue中。 ③当线程执行完任务后,会循环从LinkedBlockingQueue中获取任务来执行。
FixedThreadPool使用了LinkedBlockingQueue, 也就是无界队列(队列最大可容纳Integer.MAX_VALUE), 因此理论上任务可以无限添加,直到内存溢出;
4.3.2
|