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:一个不存储元素的无界阻塞队列,每个插入操作必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。即队列中添加任务后,必须要有线程来取走这个任务。它将任务直接提交给线程而不保持它们,如果不存在可用于立即运行任务的线程,则会构造出一个新的线程
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的原因。
关于线程池的工作流程也可以从源代码的注释中得到类似的过程