管理你的线程池(Executor)
我们都知道使用线程池能够控制线程的数量,尤其是大量的“短命”线程存在时,线程池将大大降低系统消耗(内存和CPU)。不过,线程池也同样需要管理,于是我写了本篇。
首先,我们来看看管理器的整个继承关系:
显而易见,有ThreadPoolExecutor和ScheduledThreadPoolExecutor两个实现类,当然Executor类里也有一些内部类实现了特定的功能(如class DelegatedScheduledExecutorService),我们也可以自己通过扩展这里所有的接口、抽象类、类来实现自己的特定功能,如继承ThreadPoolExecutor类,覆写beforeExecute(),让它在每个任务开始执行前执行某些操作,还有很多可扩展功能,有兴趣的朋友可以自己摸索。
你有两种方法创建上面管理器的实例:
1、你可以用上面介绍的两个类的那这些类的实例的构造函数来创建管理器的实例,不过你要自己配置一些诸如池最大尺寸(maximumPoolSize )的参数。
2、Executors提供各种创建上面的类的实例的方法,它默认一些参数的设置。我主要介绍
这种方法中的newFixedThreadPool(int)和newCachedThreadPool()
------------newFixedThreadPool(int)------------
创建一个默认尺寸的池,它同时运行的线程数将是固定的,如果你要让它课同时运行的最大线程数大于初始设置的那个参数,可以调用setMaximumPoolSize()来设置额外的线程来并行处理更多的任务。
我们调用下面的方法来添加新的任务,到底Executors是如何处理的呢?
- public void execute(Runnable command) {
- if (command == null)
- throw new NullPointerException();
- if(poolSize>=corePoolSize|| !addIfUnderCorePoolSize(command)) {
- //如果实时连接数小于corePoolSize,那么调用addIfUnderCorePoolSize()方法
- if (runState == RUNNING && workQueue.offer(command)) {
- //如果实时连接数大于了corePoolSize,那么将任务加进等待队列中。
- if (runState != RUNNING || poolSize == 0)
- //在执行workQueue.offer(command)的过程中shutdown了,确保所有的已经提交任务能够成功执行完。
- ensureQueuedTaskHandled(command);
- }
- else if (!addIfUnderMaximumPoolSize(command))
- reject(command); // is shutdown or saturated
- }
- }
- 下面我们来看下poolSize>=corePoolSize为不同状态时两种执行方法:
- private boolean addIfUnderCorePoolSize(Runnable firstTask) {
- //首先获取本类所有同步方法的锁
- Thread t = null;
- final ReentrantLock mainLock = this.mainLock;
- mainLock.lock();
- try {
- if (poolSize < corePoolSize && runState == RUNNING)
- t = addThread(firstTask);
- } finally {
- mainLock.unlock();
- }
- if (t == null)
- return false;
- t.start();
- return true;
- }
- private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
- //首先获取本类所有同步方法的锁
- Thread t = null;
- final ReentrantLock mainLock = this.mainLock;
- mainLock.lock();
- try {
- if (poolSize < maximumPoolSize && runState == RUNNING)
- t = addThread(firstTask);
- } finally {
- mainLock.unlock();
- }
- if (t == null)
- return false;
- t.start();
- return true;
- }
- 几乎完全一样,估计author Doug Lea当初也是直接copy的吧。
- 这两个方法都调用了
- private Thread addThread(Runnable firstTask) {
- Worker w = new Worker(firstTask);
- //这里并没有区分maximumPoolSize 和corePoolSize
- Thread t = threadFactory.newThread(w);
- if (t != null) {
- w.thread = t;
- workers.add(w);//workers并没有尺寸的限制
- int nt = ++poolSize;
- //这一步维护一个管理器使用过程中的最大尺寸,没什么好说的。
- if (nt > largestPoolSize)
- largestPoolSize = nt;
- }
- return t;
- }
- 于是我认为发现管理器在对待aximumPoolSize 和corePoolSize 时根本没有什么区别,可是这是不正确的,至于为什么,大家可以自己去探索!
ThreadPoolExecutor类内部有一个:
private final HashSet<Worker> workers = new HashSet<Worker>();
其中Worker类是ThreadPoolExecutor一个内部类,实现了Runable接口。在addIfUnderMaximumPoolSize()和addIfUnderCorePoolSize()两个方法中将任务添加进这个workers[]中,这个数组维护一个正在运行的任务组,这个数组中的一个元素对应一个正在运行的线程,如果一个线程以外死亡,数组中的元素没有被移除,管理器将自动创建一个新的线程继续从头开始执行刚刚那个以外死亡的数组对应的任务。
如此神奇?那是如何实现的?
很简单,ThreadPoolExecutor维护的线程的run方法都是在这个loop中的,
- while (task != null || (task = getTask()) != null) {
- runTask(task);
- task = null;
- }
如果意外死亡,task=null不执行,重新判断条件的时候再次调用runTask(task);即,死亡的是runTask(task)方法内部的run()调用而已。
说到这里,大家应该明白了,管理器无非就是用BlockingQueue<Runnable> workQueue队列(注意这个队列是线程安全的,挺有意思)来缓冲多出来的任务,而总是有不大于maximumPoolSize(注意,这里不是corePoolSize )的线程在运行着,再有点异常死亡处理的能力而已。
--------newCachedThreadPool()--------
- 这个方法源码:
- public static ExecutorService newCachedThreadPool() {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>());
- }
原来,就是让corePoolSize =0,maximumPoolSize=最大整数,然后设置空闲线程的存活时间为60s而已。看到这里,大家或许会冒出这样一个疑问:既然corePoolSize 是0,那么不是运行不了任何线程吗?呵呵,大家如果认真看了前面的文章就会有此疑问了。看同时刻运行的线程最大数是看参数maximumPoolSize不是corePoolSize 。
至于存活时间设置,那是很有必要,否则
- while (task != null || (task = getTask()) != null) {
- runTask(task);
- task = null;
- }
getTask方法中从待执行任务缓冲队列中poll()任务的时候会有一个存活时间的超时机制,如果超时将返回null,这个线程将因为一系列连锁反应,最终死亡。
好了,看似简单的Executor我砍了这么多,顺序整理的不是很好,大家将就看看吧。
总结一下,在设计这几个类的时候用到集合、同步(锁和阻塞队列)、枚举(TimeUnit)、多线程、安全控制(本文没有涉及)、工厂设计模式等等知识点,不简单哪^-^