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)

  1. 具体参数的含义如下:
  • 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:由调用线程处理该任务
  1. 在组织结构上

ThreadPoolExecutor类继承自AbstractExecutorService,AbstractExecutorService实现了ExecutorService接口,ExecutorService接口又继承自Executor接口。
顶层接口:Executor,只有execute()方法,用来定义执行任务的方法。

void execute(Runnable command);

二级接口:ExecutorService,定义了 shutdown()、submit()、invokeAll()等主要方法。
三级实现类:AbstractExecutorService,实现了ExecutorService接口中的方法。
四级实现类:ThreadPoolExecutor进一步扩展功能,主要实现了execute()方法。同时多了些状态获取,控制的方法。并且,定义了四种拒绝处理任务时的策略。

  1. execute()方法 VS submit()方法

execute()方法在Executor中声明,在ThreadPoolExecutor进行实现,通过这个方法可以向线程池提交一个任务,交由线程池去执行。没有返回值。

 submit()方法中调用了execute()方法,只是在返回值方面,使用了Future<T>

  1. 整体的运行过程__(重要!)__
  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会__采取任务拒绝策略__进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

二、任务缓存队列及排队策略

  1. 使用直接提交策略,即__SynchronousQueue__。

SynchronousQueue是无界的,也就是说他存数任务的能力是没有限制的,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加

  1. 使用无界队列策略,即__LinkedBlockingQueue__。

在达到corePoolSize线程数目之后,__因为是无界,所以任务总是可以加入队列的,因而永远也不会触发产生新的线程!__corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。如果当前线程数处理不过来,队列会出现OOM,内存溢出!

  1. 有界队列,使用__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

当然,这只是一个参考值,是具体情况而定。