为什么决定研究一下线程池?
最近在公司做个财务流水和业务充值自动匹配勾销的自动收付款系统。提测之后测试反馈了一些问题,她说她新造的数据半天都无法进行匹配。后来仔细研究了一下缘由,发现还是由于程序单线程的处理导致的效率低下,于是本人将相关的单线程模式改成了线程池ExcutorService(newFixedThreadPool)的形式,发现处理效率提高了不少,但是架构师对我写的代码进行诟病,觉得有上百万数据的时候这种线程池的处理方式肯定会有问题,于是决定细细研究一把线程池。
线程池的作用是什么?
综合罗列了一下,通俗地来说有这些作用。
(1)线程池能够比较容易地管控所执行的线程数量。
(2)根据系统环境可以设置线程数量从而达到程序更好的运行状态,能够充分合理调用cpu、内存、网络以及IO。
(3)线程池可以比较完美的去创建和销毁线程,避免频繁创建和销毁线程而浪费大量的系统资源。
关于Java的线程池框架Executor
看了下源码,简单画了一下继承关系图,具体如下:
从结构上面看其实还是很清晰的,最根本的一个接口就是Executor接口,接下来有个ExecutorService继承了该接口,并且对其接口内容进行了扩充,AbstractExecutorService抽象类实现了Executor,最终的实现是TreadPoolExecutor,之后也就不说了,其实图里面都有。
详细研究了一下Executors,这个其实是个单独的类,里面就是放置我们平时所用到的相关的线程池方法,看了下源码会发现,其实我们现实用到的几个线程池,都来源于对TreadPoolExecutor构造器不同的参数传入。
关于ThreaPoolExecutor的核心构造方法
我们详细看下ThreaPoolExecutor中的情况,可以详细地看下ThreaPoolExecutor这个类,这个类中的有很多个构造器,但是追根揭底,我们发现所有的构造器,调用的都是同一个根构造器,如下代码片段:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
具体看了下每个参数的作用:
corePoolSize:核心线程池大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:最大线程池大小。表示在线程池中最多能创建多少个线程;
keepAliveTime :线程池中超过corePoolSize之后,空闲线程的最大存活时间。表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit: keepAliveTime 的时间单位。
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:阻塞任务队列。用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
threadFactory:线程工厂,用来创建线程。
RejectedExecutionHandler:当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理。一般有以下几种处理策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
猫爸觉得看完这几个之后再去理解基础的四种线程池之后就显得是小菜一碟了,现在看来,猫爸觉得程序上面用newFixedThreadPool确实不好,程序的风险无法得到很好的控制,猫爸决定改成手动配置ThreadPoolExecutor,具体的参数放在配置文件中,这样的话风险可能会降低很多。
接下来继续看。
关于ThreaPoolExecutor的核心属性
除了之前所说的核心构造方法之外,这个类中还有这样一些属性值,如下:
第一种,应该算是线程池的状态,主要有以下这几种:
private static final int RUNNING = -1 << COUNT_BITS;//运行状态,当线程池创建时候默认值就是运行状态,可以处理新任务
private static final int SHUTDOWN = 0 << COUNT_BITS;//关闭态,不接受新任务,但处理队列中的任务
private static final int STOP = 1 << COUNT_BITS;//停止态,不接受新任务,不处理队列中任务,且打断运行中任务
private static final int TIDYING = 2 << COUNT_BITS;//整理态,所有任务已经结束,workerCount = 0 ,将执行terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;//结束态,terminated() 方法已完成
第二种,比较重要的属性,罗列一下主要有以下几种
private final BlockingQueue<Runnable> workQueue;任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();//线程池的主要状态锁,对线程池状态(比如线程池大小runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<>();//用来存放工作集
private int largestPoolSize;//用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount;用来记录已经执行完毕的任务个数
private volatile ThreadFactory threadFactory;//线程工厂,用来创建线程
private volatile RejectedExecutionHandler handler;//任务拒绝策略
private volatile long keepAliveTime;//线程存活时间
private volatile boolean allowCoreThreadTimeOut;//是否允许为核心线程设置存活时间
private volatile int corePoolSize;//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。
corePoolSize被翻译成核心池大小,猫爸觉得这个就是比较稳定的一个线程池大小。举个简单的例子:假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;然后就将任务也分配给这4个临时工人做;如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的.
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。
不过为了方便理解,在本文后面还是将corePoolSize翻译成核心池大小。
largestPoolSize只是一个用来起记录作用的变量,用来记录线程池中曾经有过的最大线程数目,跟线程池的容量没有任何关系。
第三种,线程池的执行,这是最核心的一段执行:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1、工作线程 < 核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2、运行态,并尝试将任务加入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
} // 3、使用尝试使用最大线程运行
else if (!addWorker(command, false))
reject(command);
}
具体分析下来可以得到以下一个流程: