下面开始今天的正文,线程池的核心类为ThreadPoolExecutor类,线程池基本是围绕它展开的,网上有大堆的学习资料,想快速入门,还是看JDK API,里面有详细的类说明,这里主要介绍其流程以及分析固定线程池(Executors.newFixedThreadPool)和缓存线程池(Executors.newCachedThreadPool)的原理
开始之前,先介绍一下核心线程和最大线程大小的概念:
核心线程大小(corePoolSize):线程池中存在的线程数,包括空闲线程(就是还在存活时间内,没有干活,等着任务的线程)
最大池大小(maximumPoolSize):线程池允许存在的最大线程数
ThreadPoolExecutor 将根据 corePoolSize 和 maximumPoolSize 设置的边界自动调整池大小,如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池;如果将 maximumPoolSize 设置为基本的无界值(如Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心和最大池大小仅通过构造函数来设置,不过也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。
当新任务在方法 execute(java.lang.Runnable) 中提交时,遵循以下几条规则:
规则1.如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队,即使其他辅助线程是空闲的。
规则2.如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则首选将任务添加到等待队列,而不添加新的线程。
规则3.如果无法将请求加入队列(队列满),则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝,
固定线程池(Executors.newFixedThreadPool)原理
固定线程池是怎么实现线程池固定的呢?看看他的构造函数
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
他的工作线程是固定的,而且最大线程跟核心线程数是一样的,这里就保证了线程数不会超过设定的数值,那他怎么保证任务不被reject掉呢,重点在与他的任务队列,是new LinkedBlockingQueue<Runnable>(),这是一个无界的线程安全队列,是什么意思呢,它可以存放无限个(准确说不是无限的,有个默认值Integer.MAX_VALUE的容量)任务,对照上面的规则2,只有任务队列满时才创建新线程,所以...,你懂的,这也从另外一面保障了线程数不超过设定值
再来看看缓存线程池(Executors.newCachedThreadPool),构造函数
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
神马,他的核心线程是0,初看,确定让人"大吃一斤",问题一,核心线程都没有,怎么工作?先来看看jdk api上对它的介绍:
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
重点词汇,以前的线程可用时将重用它们,问题二,怎么重用?如果没有可用线程,则创建一个,并移除超过60s未被使用的线程,问题三,怎么保证60s移除未被使用的线程。这两个问题搞清除了,带缓存功能的线程池你就搞清楚了
首先看问题一:
既然核心线程为0,那么运行线程肯定>=核心线程了,所以规则一不适用;规则二的应用就要要情况了,(1)第一个任务是肯定进不了队列的,因为缓存线程池的队列是SynchronousQueue,这个很有意思(不懂的去最开始的链接大致看一下),因为他的内部没有任何容量,只有当你正好同时使用一对操作(插入-移除)时,元素才存在,简单地说就是,当你进行offer操作时,如果正好有另外一个线程在执行插入操作时,那么恭喜你中奖了,可以拿到元素,其他时候,你都是拿不到的。所以对于第一个任务,规则2是不适用的,这时就规则3起作用了,调用addIfUnderMaximumPoolSize方法添加一个线程工作,这个线程工作完成了,不是立即就退出的,它要接着取任务,取不到任务,就等待设定的超时时间后退出,同时从缓存线程池中移除此线程。(2)其他任务大部分情况和第一个任务一样,是进不去,在有大量并发的情况下,是可能拿到任务的,这时候又要分两种情况,如果线程池中没有可用线程,则新建线程执行,如果有可用进程,刚直接在可用线程中执行任务
问题二:工作线程的重用其实是在内部类work中的run方法,如下,可以看到work类-工作线程是在不断的从队列中取任务的,想详细了解怎么取任务的可以细看下getTask方法,这样一个循环就保证了工作线程的重用,即线程执行完一个任务后,可以执行下一个,那是不是会造成死循环呢?请看问题三
/** * Main run loop */ public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } }
问题三:移除60s未使用的线程,就是在getTask方法中,下面这句是getTask等待取任务的过程,可以看到在keepAliveTime的时间内,如果没有任务进来(通过execute提前),那么这个线程会在work类的run方法中的finally中,把自己从线程池中移除,并把task置为空。ok,成功解决问题三。
...... r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS); ......
回顾整个线程池的工作原理,其实不同的线程池从构造方法上来看,就是核心线程数和最大线程数以及工作队列的不同,其中队列的应用堪称精妙,使用不同的组合就可以达到不同的效果,整个线程池设计的非常巧妙,一个类实现几种不同线程池的工作,经典。