title: Java线程池的技巧及应用小结
date: 2018-07-05 10:11:35
categories:
- 工作
tags: - Java
近来重温了一些Java方面关于线程池使用的书籍及使用场景,使用后感觉有必要整理归纳一下。
引入:我们使用线程的时候就去创建一个线程,这样实现起来非常简便。但是会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
一、基础且必须知道的
线程池主要是围绕着java.util.concurrent.ThreadPoolExecutor展开的,如果要透彻地了解Java中的线程池,必须先了解这个类。
ThreadPoolExecutor继承自AbstractExecutorService类,并提供了四个构造器。但是,仔细观察每个构造器的源码具体实现,发现前面三个构造器其实是调用的第四个构造器进行的初始化工作。
那我们直接看第四个构造器的具体功能:
undefined
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) ;
这个构造器的功能自然是创建一个线程池(Creates a new {@code ThreadPoolExecutor} with the given initial parameters)
- 具体参数的含义如下:
- corePoolSize:
the number of threads to keep in the pool,即常驻在线程池中的线程数目。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。当然也可以使用"预热"的方法:prestartCoreThread() 和 prestartAllCoreThreads(),前者是启动corePoolSize个线程,后者是启动一个线程。当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 - maximumPoolSize
表示在线程池中最多能创建多少个线程。 - keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。 - unit
参数keepAliveTime对应的时间单位,即TimeUnit类中有7种静态属性,eg:TimeUnit.SECONDS - workQueue
阻塞队列,用来存储等待执行的任务。该参数的选择会对线程池运行过程产生重大影响。具体影响在下文谈到。
一般有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
- threadFactory
线程工厂,主要用来创建线程。 - handler:表示当拒绝处理任务时的策略,有以下四种取值,具体有什么不同也会在下文详细描述。
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
- 在组织结构上
ThreadPoolExecutor类继承自AbstractExecutorService,AbstractExecutorService实现了ExecutorService接口,ExecutorService接口又继承自Executor接口。
顶层接口:Executor,只有execute()方法,用来定义执行任务的方法。
void execute(Runnable command);
二级接口:ExecutorService,定义了 shutdown()、submit()、invokeAll()等主要方法。
三级实现类:AbstractExecutorService,实现了ExecutorService接口中的方法。
四级实现类:ThreadPoolExecutor进一步扩展功能,主要实现了execute()方法。同时多了些状态获取,控制的方法。并且,定义了四种拒绝处理任务时的策略。
- execute()方法 VS submit()方法
execute()方法在Executor中声明,在ThreadPoolExecutor进行实现,通过这个方法可以向线程池提交一个任务,交由线程池去执行。没有返回值。
submit()方法中调用了execute()方法,只是在返回值方面,使用了Future<T>
- 整体的运行过程__(重要!)__
- 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
- 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
- 如果当前线程池中的线程数目达到maximumPoolSize,则会__采取任务拒绝策略__进行处理;
- 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
二、任务缓存队列及排队策略
- 使用直接提交策略,即__SynchronousQueue__。
SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。
- 使用无界队列策略,即__LinkedBlockingQueue__。
在达到corePoolSize线程数目之后,__因为是无界,所以任务总是可以加入队列的,因而永远也不会触发产生新的线程!__corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。如果当前线程数处理不过来,队列会出现OOM,内存溢出!
- 有界队列,使用__ArrayBlockingQueue__。
顾名思义,因为有界,达到corePoolSize线程数目之后,可以创建新的线程。并且,线程数目达到maximumPoolSize,之后采用拒绝策略。故而可以防止资源耗尽的情况发生。
三、拒绝策略
在ThreadPoolExecutor中定义。
四种策略
1、AbortPolicy 丢弃任务并抛出RejectedExecutionException异常(常用)。
2、DiscardPolicy 丢弃当前将要加入队列的任务本身,无异常抛出。
3、DiscardOldestPolicy 丢弃任务队列中最旧任务,随后重新尝试执行任务。
4、CallerRunsPolicy 不进入线程池执行,任务将有调用者线程去执行。
四、常见应用场景
场景
描述: 前端一次请求要求3s内返回结果,否则走打底逻辑。很多情况下前端需要的数据是由多个数据源的数据组成,且彼此间没有依赖关系。
解决思路
如果这些数据采用串行获取,大概率出现超时,所以对于没有依赖关系的数据,则可以并发执行访问。
具体执行时,可以采用,传递response引用的方式,多个数据源实例向response中放值。
main-idea:将所有submit线程的对应future,放入公共的list中,然后对该list的每个future进行get获取结果&设置超时时间。
具体步骤
第一步 规范数据源访问
定义接口: BaseBuilder<R, S>,同时定义具体方法 build方法:build(R request, S response);
数据源访问的类 用类ABuilder、BBuilder和CBuilder表示。
在本次请求中,所有需要访问数据源的类ABuilder、BBuilder和CBuilder都需要实现BaseBuilder接口和build方法。
例如:
public class ABuilder implements BaseBuilder<Request, Response> {
@Override
public void build(Request request, Response response) {
try {
// 获取A数据源的数据
A aResult = aservice.getA(request);
response.setA(aResult);
} catch(Exception e){
// 异常处理
} finally {
// 统计信息
}
}
}
第二步 定义线程池信息
定义类 BaseThreadPoolExecutor<R, S>
定义成员变量:线程池exec
private final ExecutorService exec = new ThreadPoolExecutor(32, 128, 200, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.AbortPolicy());
定义新增线程的方法:
public void addPool(List<Future> futureList,
final BaseBuilder<R, S> baseBuild, final R r, final S s) {
Future<Void> future = exec.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
baseBuild.build(r, s);
return ;
}
});
futureList.add(future);
}
获取数据源结果的方法:
/**
* 获取Future结果
* @param futureList
* @param expireTime 总超时时间
*/
public void getFutureList(List<Future> futureList, long expireTime) {
for (Future ft : futureList) {
Long start = System.nanoTime();
try {
ft.get(expireTime, TimeUnit.MILLISECONDS); //阻塞,线程等待时间
Long timeUse = System.nanoTime() - start;
expireTime = expireTime - timeUse;
if (expireTime < 0){
throw new XXXException("time out");
}
} catch (Exception e) {
ft.cancel(false);
LoggerUtils.error(logger, e, "");
break;
}
}
}
第三步 调用
实例化BaseThreadPoolExecutor,并注入
@Resource
private BaseThreadPoolExecutor<Request, Response> baseThreadPoolExecutor; //成员变量
在某个方法里开启多线程逻辑
List<Future> futureList = Lists.newArrayList();
baseThreadPoolExecutor.addPool(futureList, ABuilder, context, response);
baseThreadPoolExecutor.addPool(futureList, BBuilder, context, response);
baseThreadPoolExecutor.addPool(futureList, CBuilder, context, response);
baseThreadPoolExecutor.getFutureList(futureList, 3000000L); //超时时间为3000000s。
此时response中就有我们需要的三个数据源A、B、C的数据了。当然,如果没有就超时或者报错了。
note:共同对response操作时,最好不要对同一个成员变量操作,以防止线程安全问题的出现。
五、如何合理配置线程池的大小
一般需要根据任务的类型来配置线程池大小,目标是整体上保证CPU不要闲着:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 CPU+1
如果是IO密集型任务,参考值可以设置为2***CPU
当然,这只是一个参考值,是具体情况而定。