分享

「JDK并发包基础」线程池详解

 Bladexu的文库 2019-03-10

我们每次执行一个多线程任务时用new Thread,这样频繁创建对象会导致系统性能差,线程也缺乏统一管理,可能会无限制新建线程,相互之间竞争导致系统资源耗尽,并且缺乏定时任务,中断等功能。

线程池可以有效的提高系统资源的使用率,同时避免过多资源竞争,重用存在的线程,减少对象创建。

「JDK并发包基础」线程池详解

为了更好的控制多线程,JDK提供了一套线程框架Executor来帮助程序员有效的进行线程控制。Java.util.concurrent 包是专为 Java并发编程而设计的包,其中有一个比较重要的线程工厂类:Executors。

Java通过Executors创建不同功能的线程池,若Executors无法满足需求,我们也可以创建自定义的线程池。本文分为以下部分讲解:

  1. newFixedThreadPool()方法
  2. newSingleThreadExecutor()方法
  3. newCachedThreadPool()方法
  4. newScheduledThreadPool()方法
  5. 自定义线程池

在讲述之前,因为上面5条在方法体内部其实是创建了ThreadPoolExecutor这个类的对象,所以我们先来看看ThreadPoolExecutor中线程执行任务的示意图,它的执行任务分两种情况:

「JDK并发包基础」线程池详解

1).Execute()方法会创建一个线程然后执行一个任务。

2).这个线程在执行完1之后,会反复从BlockingQueue队列(可以看我的上一篇文章【JDK并发包基础】并发容器详解)中获取任务来执行。如果图中所示三个线程同时间在执行任务,还有任务进来则会放入BlockingQueue队列中暂缓起来等待线程空闲去执行。

再者,这3个线程正在使用,队列也满了的话(有界队列的情况),还有任务进来,则会实行拒绝策略。(take()和poll()都是取头元素节点,区别在于前者会删除元素,后者不会)

1.newFixedThreadPool()方法

创建一个固定数量的线程池,里面的线程数始终不变,当有一个线程提交时,若线程池中有空闲的线程,则立即执行。若没有,则会暂缓在一个阻塞队列LinkedBlockingQueue中等待有空闲的线程去执行。newFixedThreadPool()方法的源码如下:

「JDK并发包基础」线程池详解

现在我们思考一下:假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?

第一种方式是用join()来做,不推荐:

「JDK并发包基础」线程池详解

推荐使用线程池的方式:

「JDK并发包基础」线程池详解

运行结果如下,统计前四个盘大小可以没有顺序,但合计始终在最后:

「JDK并发包基础」线程池详解

2. newSingleThreadExecutor()方法

创建只有一个线程的线程池,若线程池中有空闲的线程,则立即执行。若没有,则会暂缓在一个阻塞队列LinkedBlockingQueue中等待有空闲的线程去执行,它保证所有任务按照提交顺序执行。我们来看看newSingleThreadExecutor方法的源码:

「JDK并发包基础」线程池详解

应用场景:这个线程池会在仅有的一个线程发生异常时,重新启动一个线程来替代原来的线程执行下去。

3.newCachedThreadPool()方法

创建一个可根据实际情况调整线程个数的线程池,不限制线程数量。若有任务,则创建线程。若无任务,则不创建线程,并且每一个空闲的线程会在60秒后自动回收。我们来看看源码:

「JDK并发包基础」线程池详解

源码中的SynchronousQueue这个没有容量的队列一创建,内部就使用take()方法阻塞着,当有一个线程来了直接就执行。

4.newScheduledThreadPool()方法

创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。它的源码如下:

「JDK并发包基础」线程池详解

源码中的DelayedWorkQueue是带有延迟时间的一个队列,其中元素只有当指定时间到了,才能够从队列中获取元素,可以做定时的功能。

创建一个任务,等3秒初始化后每隔1秒打印一句话:

「JDK并发包基础」线程池详解

这个类似于Java的Timer定时器,但项目中用Quartz,跟Spring整合的话,最好用@Scheduled注解。ref:Spring Schedule 任务调度实现

5.自定义线程池

在上述Executors工厂类创建线程池时,它的创建线程方法内部实现均用了ThreadPoolExecutor这个类,ThreadPoolExecutor可以实现自定义线程池,它的构造方法如下:

「JDK并发包基础」线程池详解

这个构造方法对于BlockingQueue队列是什么类型比较关键,它关乎这个自定义线程池的功能。

1.使用有界队列ArrayBlockingQueue时,实际线程数小于corePoolSize时,则创建线程。若大于corePoolSize时,则任务会加入BlockingQueue队列中,若队列已满,则在实际线程总数不大于maximumPoolSize时,创建新线程。若还大于maximumPoolSize,则执行拒绝策略,或者自定义的其他方式。

2.使用无界队列LinkedBlockingQueue时,缓冲队列,当实际线程超过corePoolSize核心线程数后放置等待的线程,最后等系统空闲了在这个队列里取,maximumPoolSize参数在这里就没有作用了。因为它是无界队列,所以除非系统资源耗尽,否则不会出现任务入队失败的情况。比如创建任务的速度和处理速度差异很大,无界队列会保持快速增长,直到系统内存耗尽。

有界队列和无界队列实例如下:

「JDK并发包基础」线程池详解

用LinkedBlockingQueue无界队列执行后结果是每过一段时间5个任务一执行:

「JDK并发包基础」线程池详解

对于拒绝策略,即当任务数量超过了系统实际承载能力时该如何处理呢?JDK提供了几种实现策略:

AbortPolicy:直接抛出异常来阻止系统正常工作。

CallerRunsPolicy:只要线程池未关闭,会把丢弃的任务先执行。

DiscardOledestPolicy:丢弃最老的一个请求,尝试再次提交当前任务

DiscardPolicy:丢弃无法处理的任务,不给于任何处理。

这四种策略个人觉得都不太好,我们可以实现一个自定义策略,在这里实现RejectedExecutionHandler接口就好了:

「JDK并发包基础」线程池详解

「JDK并发包基础」线程池详解

运行结果如下:

「JDK并发包基础」线程池详解

到这里已经介绍完了Java并发包下的线程池,博主是个普通的程序猿,水平有限,文章难免有错误,欢迎牺牲自己宝贵时间的读者,就本文内容直抒己见。

系列:

【JDK并发包基础】线程池详解

【JDK并发包基础】并发容器详解

【JDK并发包基础】工具类详解

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多