zoukankan      html  css  js  c++  java
  • 12.深入线程池_流程和原理

    一、线程池的基本类结构

      合理利用线程池能够带来三个好处。

      1.降低资源消耗。过重复利用已创建的线程降低线程创建和销毁造成的消耗

      2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行

      3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

      Executor线程池框架最大优点是把任务的提交和执行解耦。呵护短将要执行的任务封装成Task,然后提交即可。具体来说,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果

      下图是线程池所涉及到的所有类的结构图,先从整体把握下

    这里写图片描述 
                  图1 线程池实现原理类结构图

      上面这个图是很复杂的,涉及到了线程池内部实现原理的所有类,不利于我们理解线程池如何使用。我们先从客户端的角度出发,看看客户端使用线程池所涉及到的类结构图: 
    这里写图片描述 
                  图2 线程池使用的基本类结构图

      从图一可知,实际的线程池类是实现ExecutorService接口的类,有ThreadPoolExecutor、ForkJoinPool和ScheduledThreadPoolExecutor。下面以常用的ThreadPoolExecutor为例讲解。

    二、线程池的实现步骤

      a)线程池的创建

    1 public ThreadPoolExecutor(int corePoolSize,
    2                           int maximumPoolSize,
    3                           long keepAliveTime,
    4                           TimeUnit unit,
    5                           BlockingQueue<Runnable> workQueue,
    6                           ThreadFactory threadFactory,
    7                           RejectedExecutionHandler handler)

    注:当我们创建一个线程池的时候,并不会直接就创建出相应数量的线程

    而是,只有当提交一个任务到线程池时,在当前线程数小于线程池的基本线程数数线程池时,会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程

    如果调用了线程池的 prestarAllCoreThreads方法,线程池会提前创建并启动所有基本线程

    参数说明

      1.corePoolSize  (线程池的基本线程数)如:Executors.newFixedThreadPool(5),它的基本线程数就是5

      2.maxinumPoolSize  (线程池最大线程数)线程池允许创建的最大线程数。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果

      注:一般默认创建线程池的的时候,maxinumPoolSize  = corePoolSize  ,即任务队列满了的话,就直接拒绝了,不会创建线程

      关于任务队列,我们后面会再详解介绍

      3.keepAliveTime(线程活动保持时间) 这个参数表示,线程池的工作线程在空闲状态下,存活的时间。(默认为0,即线程闲下来就将其释放)所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率

      4.TimeUnit (线程活动保持时间的单位),这个参数是为前面那个参数服务的,默认为 毫秒

       

      5.workQueue(任务队列) 用于保存等待执行的任务的阻塞队列,当线程池中线程执行任务执行不过来的时候,会将等待执行的任务放到这个队列中,可以选择以下几个阻塞队列

    • ArrayBlockingQueue:是一个基于数组的有界阻塞队列,此队列按FIFO原则对元素进行排序
    • LinkBlockingQueue:一个基于链表结构的无界阻塞队列,此队列按FIFO排序元素,吞吐量要高于ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
      JDK中默认选用LinkedBlockingQueue作为阻塞队列的原因就在于其无界性。因为线程大小固定的线程池,其线程的数量是不具备伸缩性的,当任务非常繁忙的时候,就势必会导致所有的线程都处于工作状态,如果使用一个有界的阻塞队列来进行处理,那么就非常有可能很快导致队列满的情况发生,从而导致任务无法提交而抛出RejectedExecutionException,而使用无界队列由于其良好的存储容量的伸缩性,可以很好的去缓冲任务繁忙情况下场景,即使任务非常多,也可以进行动态扩容,当任务被处理完成之后,队列中的节点也会被随之被GC回收,非常灵活。
      
    • SynchronousQueue:一个不存储元素的无界阻塞队列,每个插入操作必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。即队列中添加任务后,必须要有线程来取走这个任务。它将任务直接提交给线程而不保持它们,如果不存在可用于立即运行任务的线程,则会构造出一个新的线程
      所以Executors.newCachedThreadPool使用了这个队列,因此它是一个缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量,它的吞吐量吞吐量通常要高于LinkedBlockingQueue
     
     
      6.ThreadFactory:用于设置创建线程的工厂,通过线程工厂给每个创建出来的线程设置更有意义的名字
     
     

      7.RejectExecutionHandler(拒绝策略):当队列和线程池都满了,什么时候会出现这种情况呢?应该是要满足: (任务队列选用的是有界的队列,任务队列满已时,且当前线程数也已经达到了线程池的最大线程数maxinumPoolSize )

      那么就必须要采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出的异常。以下是JDK1.5提供的四种策略

        AbortPolicy:直接抛出异常

        CallerRunsPolicy:只用调用者所在线程来运行任务

        DiscardOldestPolicy:丢弃队列里最后一个要执行任务,并执行当前任务

        DiscardPolicy:不处理,直接丢弃

      当然也可以根据应用场景来实现RejectedExecutionHandler接口自定义拒绝策略,如记录到日志或持久化不能处理的任务

      

      由此可见,创建一个线程所需的参数非常多,线程池为我们提供了类Executors的静态工厂方法用来创建不用类型的线程池,官方也建议我们使用它,它会给上面的参数给上一些默认值

      

      当然我们也可以自己创建线程池,自由地给定参数,来更好的适应不同的场景

      b)向线程池提交任务

      有两种方式提交任务(execute 和 submit)两者执行任务最后都会通过Executor的execute方法来执行,关于 execute方法,我们后面会详解,

      两者区别:1.异常处理,两者对待run方法抛出的异常处理方式不一样

            2.有无返回值,submit有返回值,而execute没有

      具体怎么用,可以参考上一篇文章,

      c)线程池关闭

      1.shutdown()方法

        这个方法会平滑地关闭ExecutorService,当我们调用这个方法时,ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务分两类:一类是已经在执行的,另一类是没有开始执行的),当所有已经提交的任务执行完毕后将会关闭 ExecutorService

      2.awaitTermination(long timeout,TimeUnit unit)方法

        这个方法有两个参数,一个是timeout即超时时间,另一个是unit即时间单位。这个方法会使当前关闭线程池的线程 等待 timeout时长,当超过timeout时间后,则去监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用

      3.shutdownNow()方法:这个方法会强制关闭ExecutorService,将取消所有运行中的任务和在工作队列中等待的任务,这个方法返回一个List列表,列表中返回的是等待在工作队列中任务

    三、线程池的执行流程分析

      前面提到ExecutorService的submit方法 和 execute方法都会调用Executor实现类(如ThreadPoolExecutor)的execute方法,下面我们来看看任务提交到这个方法是如何执行的,从这个方法入手分析 线程池的执行流程

      线程池的主要工作流程如下图:

      

      当提交一个新任务到线程池时,线程池的处理流程如下:

     - 首先线程池判断“基本线程池”(corePoolSize)是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。 

    - 其次线程池判断工作队列(workQueue)是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。 

    - 最后线程池判断整个线程池的线程数是否已超过maximumPoolSize?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。 

      1.提交任务

      2.如果线程数未达到corePoolSize,则创建线程执行任务

      3.如果达到corePoolSize,仍让提交了任务,则会有任务等待,所以将任务保存在任务队列中,直到任务队列workQueue已满

      4.如果workQueue已满,仍然有任务提交,但未达到最大线程数,则继续创建线程执行任务,直到线程数达到maximumPoolSize,

      5.如果达到了maximumPoolSize,则根据饱和策略拒绝该任务。这也就解释了为什么有了corePoolSize还有maximumPoolSize的原因。 

      关于线程池的工作流程也可以从源代码的注释中得到类似的过程

    参考博文:http://blog.csdn.net/mark_lq/article/details/50346999

  • 相关阅读:
    MySQL 初识别语句,数据库、表、行的增删改查
    mysql如何从全备文件中恢复单个库或者单个表
    Shell 同步时间脚本
    app手机端连接tomcat电脑端服务器
    大于号转义符&gt;---小于号转义符&lt;
    轻松实现页面提交中
    重复提交问题(一)
    json
    ExtJs 6.0+快速入门,ext-bootstrap.js文件的分析,各版本API下载(一)
    ExtJS 6 如何引入Dashboard模版
  • 原文地址:https://www.cnblogs.com/xuzekun/p/7491958.html
Copyright © 2011-2022 走看看