一、为什么要使用线程池?
一般我们使用多线程,都是创建一个线程去执行任务。在执行完后,自动销毁线程。这样看上去没有问题,但是仔细想想。如果在并发的线程数量很多的时候,每个线程执行完一段任务就结束了,这样频繁的创建线程是会大大降低服务性能和占用大量内存资源并且可能会Out Of Memory。同时大量线程回收也会给GC带来很大的压力。
这时,使用线程池来管理线程的创建和销毁就十分有必要了。简单点说,线程池就像一个加工厂,而线程池里的线程就是加工厂的员工了。当有活来了,就会从加工厂中找一个员工分配他来完成这个工作,如果做完了也不会辞退它,而是让其等待下一个任务。
二、线程池Executor总体设计
今天我们重点讲java.uitl.concurrent.ThreadPoolExecutor类,所以我们从这个类出发,去了解线程池。
总体设计架构:
本章节,我们暂时只关注下面这一条设计线就行了。
ThreadPoolExecutor(实体类)---------->AbstractExecutorService(抽象类)------------> ExecutorService(接口)------------>Executor(接口)
1.部分源码分析以及解释
public class ThreadPoolExecutor extends AbstractExecutorService { //ctl这个AtomicInteger的功能很强大,其高3位用于维护线程池运行状态,低29位维护线程池中线程数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; private static final int CAPACITY = (1 << COUNT_BITS) - 1; // runState is stored in the high-order bits //RUNNING:-1< private static final int RUNNING = -1 << COUNT_BITS; //SHUTDOWN:0< private static final int SHUTDOWN = 0 << COUNT_BITS; //STOP:1< private static final int STOP = 1 << COUNT_BITS; //TIDYING:2< private static final int TIDYING = 2 << COUNT_BITS; //TERMINATED:3< private static final int TERMINATED = 3 << COUNT_BITS; //这些状态均由int型表示,大小关系为 RUNNING // Packing and unpacking ctl //c & 高3位为1,低29位为0的~CAPACITY,用于获取高3位保存的线程池状态 private static int runStateOf(int c) { return c & ~CAPACITY; } //c & 高3位为0,低29位为1的CAPACITY,用于获取低29位的线程数量 private static int workerCountOf(int c) { return c & CAPACITY; } //参数rs表示runState,参数wc表示workerCount,即根据runState和workerCount打包合并成ctl private static int ctlOf(int rs, int wc) { return rs | wc; } //任务缓存队列,用来存放等待执行的任务 private final BlockingQueue workQueue; //线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁 private final ReentrantLock mainLock = new ReentrantLock(); //用来存放工作集 private final HashSet workers = new HashSet(); //线程存货时间 private volatile long keepAliveTime; //是否允许为核心线程设置存活时间 private volatile boolean allowCoreThreadTimeOut; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列) private volatile int corePoolSize; //线程池最大能容忍的线程数 private volatile int maximumPoolSize; //线程池中当前的线程数 private volatile int poolSize; //任务拒绝策略 private volatile RejectedExecutionHandler handler; //线程工厂,用来创建线程 private volatile ThreadFactory threadFactory; //用来记录线程池中曾经出现过的最大线程数 private int largestPoolSize; //用来记录已经执行完毕的任务个数 private long completedTaskCount; public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); } 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:
核心池大小,简单的来说,就像上面说的如果加工厂是线程池,那么工厂的员工数量就是加工厂的corePoolSize了。毕竟一个加工厂,必须要有员工才能工作嘛~~
maximumPoolSize:
线程池的最大线程数,如果加工厂任务太多,当前的员工根本不够完成的时候,这是厂长就出来说:“那个谁给我找几个临时工来帮忙”。但是为了控制人口成本,老板就定了一个工作的人员上限。并给那个谁说,我们现在已经有corePoolSize个员工了,你在招人不能够超过maximumPoolSize了。
keepAliveTime:
表示线程没有任务执行时最多保持多久时间会终止。当加工厂加班任务完成了,之前招的临时工就该从哪里来回哪里去了。毕竟老板说了,控制人员成本(开源节流),公司不养闲人~~~
unit:
参数keepAliveTime的时间单位(天,时,分,秒…)
workQueue:
一个阻塞队列,用来存储等待执行的任务。有一天,加工厂来了平常两倍的货物要加工。这是有人给老板反馈,老板人手不够呀,我们去招人帮忙吧。老板反手一巴掌,说到招人不要钱呀。去把另一半货物放到另一个备用仓库(workQueue)放着,然后对着员工说,今天谁先做完手上的活,重重有赏~~~。员工一听立马工作效率就是翻倍,一个个拼命的工作。不一会儿,员工就把手上的工作做完了,开心的询问老板赏啥呀~~~老板开心的叫到另一个仓库的一半货物拉出来,并说道干不完谁都不许走~ 此时,十万个草泥马奔腾而过,惨呀!!!
注:阻塞队列一般三种:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue;
threadFactory:
线程工厂,主要用来创建线程;管人事的大大~~~
handler:
表示当拒绝处理任务时的策略。当加工厂的临时工招满了,对前来应聘的人委婉的拒绝(不发工资,了解一下)。
一般拒绝策略有四种:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
简单总结一下线程池工作原理流程:
简单来说当使用线程池的时候,如果有任务需要执行,会判断当前是否还有空闲的线程可用。如果没有就会把任务放到阻塞队列中去,等待其他正在工作线程完成任务后,再来执行。如果阻塞队列任务放满了,这时又来新任务了,就不得不去创建新的线程去执行新任务了。但是这个创建的线程是有限的(线程池的线程数量不能大于线程池最大线程数(maximumPoolSize)。一旦新任务过多,线程数已达到最大(maximumPoolSize),这时就需要使用拒绝策略去拒绝新来的任务了。
线程池中大部分线程任务都执行完毕后,而且线程也没有新的任务,这时根据keepAliveTime存活时长,干掉一些不用的线程,使其线程保持在corePoolSize数量。
2.深入剖析线程池实现原理
execute()源码加注释
public void execute(Runnable command) { // 如果任务为null,则抛出异常。 if (command == null) throw new NullPointerException(); // 获取ctl对应的int值。该int值保存了"线程池中任务的数量"和"线程池状态"信息 int c = ctl.get(); // 当线程池中的任务数量 < "核心池大小"时,即线程池中少于corePoolSize个任务。 // 则通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } // 当线程池中的任务数量 >= "核心池大小"时, // 而且,"线程池处于允许状态"时,则尝试将任务添加到阻塞队列中。 if (isRunning(c) && workQueue.offer(command)) { // 再次确认“线程池状态”,若线程池异常终止了,则删除任务;然后通过reject()执行相应的拒绝策略的内容。 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); // 否则,如果"线程池中任务数量"为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。 else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。 // 如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。 else if (!addWorker(command, false)) reject(command); }
上图中,我已经很详细的加了注释了。大致过程为:
先判断执行任务是否存在,存在则判断当前线程是否小于核心线程数。
如果比核心线程数少,则直接创建线程执行任务。
否则,需要再判断工作队列是否放满了,如果没有则放入工作队列中,等待其他线程从工作队列中取出任务执行。
如果满了,则还需要判断当前线程数是否超过现线程池最大线程数(maxPoolSize),超过则拒绝任务。
addWorker()方法
private boolean addWorker(Runnable firstTask, boolean core) { retry: // 更新"线程池状态和计数"标记,即更新ctl。 for (;;) { // 获取ctl对应的int值。该int值保存了"线程池中任务的数量"和"线程池状态"信息 int c = ctl.get(); // 获取线程池状态。 int rs = runStateOf(c); // 有效性检查 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { // 获取线程池中任务的数量。 int wc = workerCountOf(c); // 如果"线程池中任务的数量"超过限制,则返回false。 if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; // 通过CAS函数将c的值+1。操作失败的话,则退出循环。 if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl // 检查"线程池状态",如果与之前的状态不同,则从retry重新开始。 if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } boolean workerStarted = false; boolean workerAdded = false; Worker w = null; // 添加任务到线程池,并启动任务所在的线程。 try { final ReentrantLock mainLock = this.mainLock; // 新建Worker,并且指定firstTask为Worker的第一个任务。 w = new Worker(firstTask); // 获取Worker对应的线程。 final Thread t = w.thread; if (t != null) { // 获取锁 mainLock.lock(); try { int c = ctl.get(); int rs = runStateOf(c); // 再次确认"线程池状态" if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); // 将Worker对象(w)添加到"线程池的Worker集合(workers)"中 workers.add(w); // 更新largestPoolSize int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { // 释放锁 mainLock.unlock(); } // 如果"成功将任务添加到线程池"中,则启动任务所在的线程。 if (workerAdded) { t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } // 返回任务是否启动。 return workerStarted; }
简单说一下,addWorker方法。
首先会判断一下当前线程池的状态以及工作队列是否满足添加一个worker。
如果满足,则需要判断是否会超出线程池的最大上限。
如果不超过,则通过CSA乐观锁操作,去添加一个woker。
需要先对线程池上锁,把woker放入线程池中,然后解锁。
最后启动线程,执行woker任务。
注意:Worker类本身既实现了Runnable,又继承了AbstractQueuedSynchronizer(以下简称AQS),所以其既是一个可执行的任务,又可以达到锁的效果。
shutdown()方法
public void shutdown() { final ReentrantLock mainLock = this.mainLock; // 获取锁 mainLock.lock(); try { // 检查终止线程池的“线程”是否有权限。 checkShutdownAccess(); // 设置线程池的状态为关闭状态。 advanceRunState(SHUTDOWN); // 中断线程池中空闲的线程。 interruptIdleWorkers(); // 钩子函数,在ThreadPoolExecutor中没有任何动作。 onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { // 释放锁 mainLock.unlock(); } // 尝试终止线程池 tryTerminate(); }
三、总结
首先总结一下线程池,其设计思路为:线程池为一个创建管理线程的工厂,把执行任务分配给各个线程去执行,当线程数量不满足任务数量时。会暂时把任务防止阻塞队列中,等待空闲线程来执行。当阻塞队列放置满了,就会创建新线程去执行多出的任务。当任务足够多,以至于创建的线程达到最大线程数时,就会采用拒绝策略不接收任务。最后,当任务量小了,大部分线程执行完任务后,转变为待执行的线程时。会清理掉一些线程数量,以保证线程利用率和降低资源开销。
线程池的东西还有很多,实际运用中也会遇到很多很多问题。本片只是基础介绍,难以讲解多线程实际运用中遇见的各种问题。
到这里线程池的介绍也就基本结束了,希望看到这里大家都有自己的收获。初入并发编程学习之门,很多东西理解不深。自身你能力有限,只能理解到这里了,还望见谅~~~