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

原文地址:https://www.cnblogs.com/xuzekun/p/7491958.html