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 CacheThreadPool
// 构造方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
通过构造方法可以看出:CacheThreadPool特点为:①核心线程数为0;②最大线程数为MAX_VALUE,可以理解为不设限;③非核心线程超过60s无任务执行会被回收;④任务队列类型为SynchronousQueue;
SynchronousQueue是一种不存储任务的阻塞队列,他的插入和删除是互斥的,也就是插入新任务的同时不允许执行移除任务操作。所以当一个新任务进入时:CacheThreadPool的处理流程应当如下:由于核心线程数始终为0,且任务队列为无界队列,所以新任务会通过SynchronQueue().offer方法加进任务队列中,接着检查当前是否有空闲线程,如果有则直接将新任务交给空闲线程去执行,如果没有则创建一个新线程并执行该任务;当有一个线程空闲下来时,会通过SynchronQueue().poll方法取任务去执行,如果超过60s都无法取到任务,则销毁当前线程;CachedThreadPool 比较适于大量的需要立即处理并且耗时较少的任务。
4.3.3 SingleThreadExecutor
// 构造方法
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
通过构造方法可以看出,SingleThreadExecutor:①核心线程数=最大线程数=1的线程池;② 线程超时回收时间为0(即无任务执行时线程立刻被回收);③消息队列和FixedThreadPool一样,为无界链式消息队列;
SingleThreadExecutor执行任务流程为,一个新任务进来后,首先判断当前是否存在线程,如果不存在则创建一个线程(当然,最多也只能存在一个)执行新任务,如果当前已有线程(如果有线程存在,则该线程一定正在执行任务,因为从构造方法中我们知道,线程执行完任务会立即被回收)则将新任务加到队列中,由于队列本身是有序的,所以SingleThreadExecutor实际上保证了所有任务按照进入顺序依次执行。
4.3.4 ScheduledThreadPool
// 构造方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
从构造方法可以看出ScheduledThreadPool实际上是构造了ScheduledThreadPoolExecutor(),这点是与前面说到的三个线程池都不同的,前三个都是构造ThreadPoolExecutor(),而实际上ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,主要封装了延迟,定时相关的功能,其构造方法如下:
// ScheduledThreadPoolExecutor的构造
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
可以看到实际上是通过super构造了ThreadPoolExecutor
,结合这两层套娃,我们可以得出ScheduledThreadPool
的特点如下:
① 最大线程数为Interger.MAX_VALUE
,即溢出前不设限;②超时时间为0;③消息队列为DelayWorkQueue();