• java线程池原理


    本文原创,转载请注明来自xiaoQLu http://www.cnblogs.com/xiaoQLu/archive/2013/05/13/2870588.html

    首先介绍一篇系统学习concurrency包的博文,有兴趣的可以细读下,写得很不错 http://www.blogjava.net/xylz/archive/2010/07/08/325587.html,其中包括缓存线程池的核心队列的介绍 http://www.blogjava.net/xylz/archive/2010/07/30/327582.html 

    下面开始今天的正文,线程池的核心类为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);
    ......

    回顾整个线程池的工作原理,其实不同的线程池从构造方法上来看,就是核心线程数和最大线程数以及工作队列的不同,其中队列的应用堪称精妙,使用不同的组合就可以达到不同的效果,整个线程池设计的非常巧妙,一个类实现几种不同线程池的工作,经典。

    至此,线程池工作原理告一段落,讲得不清楚的地方欢迎留言讨论。

    附几个讲原理比较不错的网址:

    Java多线程之线程池深入分析-推荐 http://blog.csdn.net/a511596982/article/details/8299108 http://blog.csdn.net/a511596982/article/details/8299108,写得很详细,值得细看

    源码分析,整个ThreadPoolExecutor的java文件分析,可以作为分析的工具 http://www.cnblogs.com/rilley/archive/2012/02/07/2341767.html

    线程池分析 http://xtu-xiaoxin.iteye.com/blog/647744 这个讲的比较笼统,适合想快速了解原理的人

  • 相关阅读:
    c++ -- c风格字符串,以及c++通过c_str()方法与c风格字符串的转换
    python--sklearn,聚类结果可视化工具TSNE
    ml--分类与预测算法评价方法
    python--pandas学习,依赖库xlrd和xlwt
    python--源码位置查找
    mysql中的group by,having,order by,where用法
    mysql外连接的总结
    mysql数据库中多表关联查询的实例
    理解mysql数据库的事务特征,事务隔离级别,加锁机制
    理解springMVC的controller
  • 原文地址:https://www.cnblogs.com/xiaoQLu/p/2870588.html
Copyright © 2020-2023  润新知