引言
在 多线程(一) 多线程介绍及基本使用中我提及了线程池及常见的四种线程池,在这章推荐大神的博文,希望大家共同学习,讨论!
本文原始地址:http://blog.csdn.net/ghsau/article/details/53538303。
回顾四种常见线程池:
常见线程池:
①newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
②newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
③newCacheThreadExecutor(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
④newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程
ExecutorService pool = Executors.常见线程池
例:ExecutorService pool = Executors.newSingleThreadExecutor();
一 线程池的作用
线程池的作用:
线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
<1>减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
<2>可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机);
二 线程池分析
类结构:
这里面的实现类涉及到三个:
ForkJoinPool:一个类似于Map/Reduce模型的框架,线程级的,Fork/Join-Java并行计算框架,推荐博客:Fork/Join-Java并行计算框架。
ThreadPoolExecutor:这是Java线程池的实现,也是本文的主角,Executors提供的几种线程池主要使用该类。
ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,添加了调度功能。
ThreadPoolExecutor参数
private volatile int corePoolSize;
线程池基本大小
private volatile int maximumPoolSize;
线程池最大大小
private volatile long keepAliveTime;
保持活动时间
TimeUnit unit –(public enum TimeUnit)
保持活动时间单位
private final BlockingQueue workQueue;
工作队列
ThreadFactory threadFactory –(public interface ThreadFactory)
线程工厂
RejectedExecutionHandler handler –(public interface RejectedExecutionHandler)
驳回回调
这些参数这样描述起来很空洞,下面结合执行任务的流程来看一下(更多参数请多去看ThreadPoolExecutor源码,多翻翻资料)。
ThreadPoolExecutor执行任务流程
当我们调用execute方法时,这个流程就开始了,请看下图:
当线程池大小 >= corePoolSize 且 队列未满时,这时线程池使用者与线程池之间构成了一个生产者-消费者模型。线程池使用者生产任务,线程池消费任务,任务存储在BlockingQueue中,注意这里入队使用的是offer,当队列满的时候,直接返回false,而不会等待,有关BlockingQueue推荐文章阻塞队列BlockingQueue。
keepAliveTime
当线程处于空闲状态时,线程池需要对它们进行回收,避免浪费资源。但空闲多长时间回收呢,keepAliveTime就是用来设置这个时间的。默认情况下,最终会保留corePoolSize个线程避免回收,即使它们是空闲的,以备不时之需。但我们也可以改变这种行为,通过设置allowCoreThreadTimeOut(true)。
RejectedExecutionHandler
当队列满 且 线程池大小 >= maximumPoolSize时会触发驳回,因为这时线程池已经不能响应新提交的任务,驳回时就会回调这个接口rejectedExecution方法,JDK默认提供了4种驳回策略,代码比较简单,直接上代码分析,具体使用何种策略,应该根据业务场景来选择,线程池的默认策略是AbortPolicy。
ThreadPoolExecutor.AbortPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 直接抛出运行时异常
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
ThreadPoolExecutor.CallerRunsPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 转成同步调用
r.run();
}
}
ThreadPoolExecutor.DiscardPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 空实现,意味着直接丢弃了
}
ThreadPoolExecutor.DiscardOldestPolicy
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 取出队首,丢弃
e.getQueue().poll();
// 重新提交
e.execute(r);
}
}
Hook methods
ThreadPoolExecutor预留了以下三个方法,我们可以通过继承该类来做一些扩展,比如监控、日志等等。
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Thread t, Runnable r) { }
protected void terminated() { }
ThreadPoolExecutor状态
线程池的工作流程我们应该大致清楚了,其内部同时维护了一个状态,现在来看一下每种状态对于任务会造成什么影响以及状态之间的流转。
RUNNING
初始状态,接受新任务并且处理已经在队列中的任务。
SHUTDOWN
不接受新任务,但处理队列中的任务。
STOP
不接受新任务,不处理排队的任务,并中断正在进行的任务。
TIDYING
所有任务已终止,workerCount为零,线程转换到状态TIDYING,这时回调terminate()方法。
TERMINATED
终态,terminated()执行完成。
上图是这5种状态间的流转,可以看到它们是单向的、不可逆的。
扩展
Tomcat线程池
Dubbo线程池
这两种线程池都是使用ThreadPoolExecutor来实现的,去看它们是如何使用的,有助于我们更好的理解线程池。
总结
现在我们在回过头来去看Executors中提供的几种线程池(fixed、cached、single),如果你能回答出下面几个问题,说明你明白了线程池。
为什么newFixedThreadPool中要将corePoolSize和maximumPoolSize设置成一样?
为什么newFixedThreadPool中队列使用LinkedBlockingQueue?
为什么newFixedThreadPool中keepAliveTime会设置成0?
为什么newCachedThreadPool中要将corePoolSize设置成0?
为什么newCachedThreadPool中队列使用SynchronousQueue?
为什么newSingleThreadExecutor中使用DelegatedExecutorService去包装ThreadPoolExecutor?
可能到这里会有人问,讲了这么多,我应该如何去选择线程池?线程池应该设置多大?没有固定的答案,只有适合的答案,下面说一下我的理解:
关于线程池大小问题,可以参考这个公式,仅仅是参考而已。
启动线程数 = [ 任务执行时间 / ( 任务执行时间 - IO等待时间 ) ] x CPU内核数
在控制线程池大小的基础上,尽量使用有界队列并且设置大小,避免OOM。
设置合理的驳回策略,适用于你的业务。