最近在看“阿里java编程规范”,看到关于线程池使用的一个建议,发现自己对这块貌似理解的有点模糊,特意看了下相关实现,顺便来写一篇笔记,梳理下自己的思路。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
通过查看源码,我们会发现Executors内的方法实际上也是使用ThreadPoolExecutor来创建线程池的,只是使用ThreadPoolExecutor创建线程池的参数较多,Executors就提供了几种常用的配置来简化这个创建过程。但这样也导致了大量程序员只是简单的了解如何通过Executors提供的静态方法创建想要的线程池,却忽略了对线程池更深入的理解,不知道这些方法存在场景的限制,在处理任务数量较多且任务处理比较耗时时,线程池就可能因为没有设置线程数量或等待队列长度而导致出现OOM。
我们先来介绍下ThreadPoolExecutor构造函数内的参数分别代表什么:
corePoolSize 线程池维护线程的最少数量
maximumPoolSize 线程池维护线程的最大数量
keepAliveTime 线程池维护线程所允许的空闲时间
unit 线程池维护线程所允许的空闲时间的单位
workQueue 线程池所使用的等待队列
threadFactory 创建新线程使用的工厂
handler 线程池对拒绝任务的处理策略
下面我们分析下线程池是如何处理新添加进来的任务的:
(线程池内)线程数小于corePoolSize,则新建一个线程处理;
线程数等于corePoolSize
a)等待队列可入队,则将任务放入等待队列
b)等待队列已满,线程数小于maximumPoolSize,则新建线程处理;线程 数等于maximumPoolSize,则调用handler.rejectedExecution进行拒绝提交任务的处理
池中当前有大于corePoolSize的线程,则这些多出的线程在空闲时间过keepAlivTime时将会终止。
从上面的流程可以看出,线程池会优先创建corePoolSize个线程响应新加入的任务,在corePoolSize个线程处理不完的情况下,会将线程加入等待队列,在等待队列满之后,才会尝试继续增加线程来处理,直到线程数达到maximumPoolSize,再有更多任务到达时,就会进行拒绝任务提交的处理。
这里就有两个关注点maximumPoolSize和workQueue的长度,当workQueue是无界队列时,corePoolSize个线程处理不完的任务会一直在队列内累积,线程池不会创建更多的线程处理,当队列累积到一定程度时,就很可能出现OOM(如FixedThreadPool);而当workQueue长度固定时,如果maximumPoolSize的大小不限,就会导致线程池一直新建线程来处理新增的任务,结果也很可能因大量的线程导致OOM(如CachedThreadPool)。
在任务数量可能较大的场景下,需要限制好线程池内的线程数和等待队列的长度,避免大量任务提交时,线程池不限数量的创建线程或者等待队列内的任务数持续累积,最终导致OOM。
这里就涉及到一个系统设计里面的一个很重要的点,每个模块都有其服务能力的上限,在设计时,要考虑到这一点,当超过服务能力时,应该给出一个处理方式(fail-fast),而不是拖着整个系统一起挂掉。编程规范里这一条的建议实际上说的也是这一点。在我们日常编程时,应该让程序在可控范围之内,避免某些情况下程序占用资源无限制的增长,导致整个系统不可用。
线程池允许提交的任务分为两类Runnable和Callable,Runnable接口大家应该挺熟悉,是创建多线程常用的一种方式;Callable比较特殊,它允许获取异步任务执行的结果,运行Callable任务通常有两种方式,一种是使用线程池的submit方法执行,一直是封装为FutureTask对象,通过Thread来启动,代码分别如下:
CustomCallable callable = new CustomCallable();
Future<String> future = executorService.submit(callable);
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
可以通过future获取线程执行状态,取消任务,并在合适的时候获取任务执行结果。
FutureTask对象实现了RunnableFuture接口,RunnableFuture继承了Runnable和Future接口,同时具有了这两个接口的功能,线程池内部处理callable任务时也是将其转化为FutureTask对象来进行后续处理的。
建议对这块知识了解不透彻的同学,还是自己动手练习一下,体验下通过Executors(创建FixedThreadPool,SingleThreadPool,CachedThreadPool,ScheduledThreadPool)和ThreadPoolExecutor创建的不同之处。最好是整体过一下源码,并写一篇自己的笔记,帮助自己系统的梳理一下相关的知识。
相关阅读:
http://blog.csdn.net/wangwenhui11/article/details/6760474 线程池的内容讲的比较全面细致
http://blog.csdn.net/ghsau/article/details/7451464 Callable和runnable