为什么要使用线程池

平时讨论多线程处理,大佬们必定会说使用线程池,那为什么要使用线程池?其实,这个问题可以反过来思考一下,不使用线程池会怎么样?当需要多线程并发执行任务时,只能不断的通过new Thread创建线程,每创建一个线程都需要在堆上分配内存空间,同时需要分配虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间,当这个线程对象被可达性分析算法标记为不可用时被GC回收,这样频繁的创建和回收需要大量的额外开销。再者说,JVM的内存资源是有限的,如果系统中大量的创建线程对象,JVM很可能直接抛出OutOfMemoryError异常,还有大量的线程去竞争CPU会产生其他的性能开销,更多的线程反而会降低性能,所以必须要限制线程数。

既然不使用线程池有那么多问题,我们来看一下使用线程池有哪些好处:

使用线程池可以复用池中的线程,不需要每次都创建新线程,减少创建和销毁线程的开销;

同时,线程池具有队列缓冲策略、拒绝机制和动态管理线程个数,特定的线程池还具有定时执行、周期执行功能,比较重要的一点是线程池可实现线程环境的隔离,例如分别定义支付功能相关线程池和优惠券功能相关线程池,当其中一个运行有问题时不会影响另一个。

如何构造一个线程池对象

本文内容我们只聊线程池ThreadPoolExecutor,查看它的源码会发现它继承了AbstractExecutorService抽象类,而AbstractExecutorService实现了ExecutorService接口,ExecutorService继承了Executor接口,所以ThreadPoolExecutor间接实现了ExecutorService接口和Executor接口,它们的关系图如下。

一般我们使用的execute方法是在Executor接口中定义的,而submit方法是在ExecutorService接口中定义的,所以当我们创建一个Executor类型变量引用ThreadPoolExecutor对象实例时可以使用execute方法提交任务,当我们创建一个ExecutorService类型变量时可以使用submit方法,当然我们可以直接创建ThreadPoolExecutor类型变量使用execute方法或submit方法。

ThreadPoolExecutor定义了七大核心属性,这些属性是线程池实现的基石。

corePoolSize(int):核心线程数量。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到任务队列当中。线程池将长期保证这些线程处于存活状态,即使线程已经处于闲置状态。除非配置了allowCoreThreadTimeOut=true,核心线程数的线程也将不再保证长期存活于线程池内,在空闲时间超过keepAliveTime后被销毁。

workQueue:阻塞队列,存放等待执行的任务,线程从workQueue中取任务,若无任务将阻塞等待。当线程池中线程数量达到corePoolSize后,就会把新任务放到该队列当中。JDK提供了四个可直接使用的队列实现,分别是:基于数组的有界队列ArrayBlockingQueue、基于链表的无界队列LinkedBlockingQueue、只有一个元素的同步队列SynchronousQueue、优先级队列PriorityBlockingQueue。在实际使用时一定要设置队列长度。

maximumPoolSize(int):线程池内的最大线程数量,线程池内维护的线程不得超过该数量,大于核心线程数量小于最大线程数量的线程将在空闲时间超过keepAliveTime后被销毁。当阻塞队列存满后,将会创建新线程执行任务,线程的数量不会大于maximumPoolSize。

keepAliveTime(long):线程存活时间,若线程数超过了corePoolSize,线程闲置时间超过了存活时间,该线程将被销毁。除非配置了allowCoreThreadTimeOut=true,核心线程数的线程也将不再保证长期存活于线程池内,在空闲时间超过keepAliveTime后被销毁。

TimeUnit unit:线程存活时间的单位,例如TimeUnit.SECONDS表示秒。

RejectedExecutionHandler:拒绝策略,当任务队列存满并且线程池个数达到maximunPoolSize后采取的策略。ThreadPoolExecutor中提供了四种拒绝策略,分别是:抛RejectedExecutionException异常的AbortPolicy(如果不指定的默认策略)、使用调用者所在线程来运行任务CallerRunsPolicy、丢弃一个等待执行的任务,然后尝试执行当前任务DiscardOldestPolicy、不动声色的丢弃并且不抛异常DiscardPolicy。项目中如果为了更多的用户体验,可以自定义拒绝策略。

threadFactory:创建线程的工厂,虽说JDK提供了线程工厂的默认实现DefaultThreadFactory,但还是建议自定义实现最好,这样可以自定义线程创建的过程,例如线程分组、自定义线程名称等。

一般我们使用类的构造方法创建它的对象,ThreadPoolExecutor提供了四个构造方法。

了解了线程池ThreadPoolExecutor的基本构造,接下来手撸一段代码看看如何使用,样例代码中的参数仅为了配合原理解说使用。

线程池工作原理

关于线程池的工作原理,我用下面的7幅图来展示。
1.通过execute方法提交任务时,当线程池中的线程数小于corePoolSize时,新提交的任务将通过创建一个新线程来执行,即使此时线程池中存在空闲线程。

2.通过execute方法提交任务时,当线程池中线程数量达到corePoolSize时,新提交的任务将被放入workQueue中,等待线程池中线程调度执行。

3.通过execute方法提交任务时,当workQueue已存满,且maximumPoolSize大于corePoolSize时,新提交的任务将通过创建新线程执行。

4.当线程池中的线程执行完任务空闲时,会尝试从workQueue中取头结点任务执行。

5.通过execute方法提交任务,当线程池中线程数达到maxmumPoolSize,并且workQueue也存满时,新提交的任务由RejectedExecutionHandler执行拒绝操作。

6.当线程池中线程数超过corePoolSize,并且未配置allowCoreThreadTimeOut=true,空闲时间超过keepAliveTime的线程会被销毁,保持线程池中线程数为corePoolSize。

注意:上图表达的是销毁空闲线程,保持线程数为corePoolSize,不是销毁corePoolSize中的线程。

7.当设置allowCoreThreadTimeOut=true时,任何空闲时间超过keepAliveTime的线程都会被销毁。

线程池底层实现原理

查看ThreadPoolExecutor的源码,发现ThreadPoolExecutor的实现还是比较复杂的,下面简单介绍几个重要的全局常量和方法。

ctl用于表示线程池的状态和线程数,在ThreadPoolExecutor中使用32位二进制数来表示线程池的状态和线程池中线程数量,其中前3位表示线程池状态,后29位表示线程池中线程数。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))初始化线程池状态为RUNNING、线程池数量为0。

COUNT_BITS值等于Integer.SIZE - 3,在源码中Integer.SIZE是32,所以COUNT_BITS=29。CAPACITY表示线程池允许的最大线程数,转算后的结果如下。
RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED分别表示线程池的不同状态,转算后的结果如下。


线程池不同状态之间的转换时机及转换关系如下图。

runStateOf获取ctl高三位,也就是线程池的状态。workerCountOf获取ctl低29位,也就是线程池中线程数。ctlOf计算ctlOf新值,也就是线程池状态和线程池个数。

你可能会疑问“为什么要介绍上面这些?”,这是因为接下来的源码分析会用到这些基础的知识点。一般,我们使用ThreadPoolExecutor的execute方法提交任务,所以从execute的源码入手。

为了更轻松的理解上图中的源码,我又画了一个流程图。

到这里线程池的基本实现原理已经很清晰了,接下来我们重点分析一下线程池中线程是如何执行任务、如何复用线程和线程空闲时间超限如何判断的。还是从execute方法入手,我们直接看它里面调用的addWorker方法,它实现了创建新线程执行任务。

源码中将线程和任务封装到了Worker中,然后将Worker添加到HashSet集合中,添加成功后通过线程对象的start方法启动线程执行任务,既然这样那我们就来看看上图代码中的w = new Worker(firstTask)到底是如何执行的。

Worker继承了AbstractQueuedSynchronizer,并且实现了Runnable接口,看到这里很清楚了任务最终由Worker中的run方法执行,而run方法里调用了runWorker方法,所以重点还是runWorker方法。

在runWorker方法中,使用循环,通过getTask方法,不断从阻塞队列中获取任务执行,如果任务不为空则执行任务,这里实现了线程的复用,不断的获取任务执行,不用重新创建线程;队列中获取的任务为null,则将Worker从HashSet集合中清除,注意这个清除就是空闲线程的回收。那getTask何时返回null?接着看getTask源码。


使用构造方法创建线程池
细心的朋友会发现,全文竟没有介绍Executors,这个创建线程池的辅助工具类。是的,我强烈不推荐使用它,因为Executors中的newFixedThreadPool和newSingleThreadExecutor方法创建的线程池中,阻塞队列LinkedBlockingQueue的长度是Integer.MAX_VALUE,可能会堆积大量的任务,从而导致 OOM;而newCachedThreadPool方法创建的线程池中最大线程数是Integer.MAX_VALUE,会创建大量的线程,从而导致OOM。如果创建线程池,通过ThreadPoolExecutor的构造方法创建,这样使用这个线程池的人会更加明确线程池的各个参数的设置及运行方式,提前避免隐藏问题的发生。

使用自定义线程工厂
为什么要这么做呢?是因为,当项目规模逐渐扩展,各系统中线程池也不断增多,当发生线程执行问题时,通过自定义线程工厂创建的线程设置有意义的线程名称可快速追踪异常原因,高效、快速的定位问题。

使用自定义拒绝策略
虽然,JDK给我们提供了一些默认的拒绝策略,但我们可以根据项目需求的需要,或者是用户体验的需要,定制拒绝策略,完成特殊需求。

线程池划分隔离
不同业务、执行效率不同的分不同线程池,避免因某些异常导致整个线程池利用率下降或直接不可用,进而影响整个系统或其它系统的正常运行。

线程池工作原理和实现原理相关推荐

  1. 【源码阅读计划】浅析 Java 线程池工作原理及核心源码

    [源码阅读计划]浅析 Java 线程池工作原理及核心源码 为什么要用线程池? 线程池的设计 线程池如何维护自身状态? 线程池如何管理任务? execute函数执行过程(分配) getTask 函数(获 ...

  2. 25张图展示线程池工作原理和实现原理,建议认真阅读,对你有帮助

    上篇<这样的API网关查询接口优化,我是被迫的>文章末尾,有朋友留言提到文中的场景是IO密集型操作,不是CPU密集操作,不需要使用线程池,我猜这位朋友可能想表达的是IO密集且阻塞时间久的不 ...

  3. 线程池工作原理流程图 源码概览线程池工作原理流程图 源码概览

    故事讲完啦,再复习下线程池工作流程图吧~ 有兴趣的朋友,源码也看下吧~ if (command == null)throw new NullPointerException();int c = ctl ...

  4. Java线程池,从使用到原理

    转载自  Java线程池,从使用到原理 线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象 ...

  5. 图解线程池——清新脱俗的讲原理

    网上介绍线程池的文章很多,质量好坏不一.能讲的很透彻的,确实不多. 本人能力有限,本文先从原理入手,讲清楚线程池是怎么运行的. 至于源码的分析,将单独写一篇(<线程池源码详解>). 全文以 ...

  6. 【Android 异步操作】线程池 ( 线程池作用 | 线程池种类 | 线程池工作机制 | 线程池任务调度源码解析 )

    文章目录 一.线程池作用 二.线程池种类 三.线程池工作机制 四.线程池任务调度源码解析 一.线程池作用 线程池作用 : ① 避免创建线程 : 避免每次使用线程时 , 都需要 创建线程对象 ; ② 统 ...

  7. 自定义线程池-线程池工作流程介绍

    ThreadPoolExecutor参数详解 我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数; a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位 ...

  8. java线程池工作原理和实现原理

    为什么要使用线程池? 1.使用线程池可以复用池中的线程,不需要每次都创建新线程,减少创建和销毁线程的开销: 2.同时,线程池具有队列缓冲策略.拒绝机制和动态管理线程个数,特定的线程池还具有定时执行.周 ...

  9. java 池化_溯本求源: JAVA线程池工作原理

    1. 前言 线程池是JAVA开发中最常使用的池化技术之一,可以减少线程资源的重复创建与销毁造成的开销. 2. 灵魂拷问:怎么做到线程重复利用? 很多同学会联想到连接池,理所当然的说:需要的时候从池中取 ...

最新文章

  1. Zookeeper源码分析:Leader角色初始化
  2. python输入输出重定向_Python标准输出重定向
  3. mysql 5.7临时表空间_深度解析MySQL 5.7之临时表空间
  4. python文件读取输出-python分批定量读取文件内容,输出到不同文件中的方法
  5. python练习册 每天一个小程序 第0009题
  6. PCANet --- 用于图像分类的深度学习基准
  7. 数据结构复习—1.1
  8. Spring5源码 - 14 如何在所有Bean创建完后做扩展?
  9. Programming with gtkmm 3
  10. Web开发者不可不知的16条原则
  11. SQL2000 MD5加密
  12. 外部仓库_仓库主要作业流程和WMS作业优化方案
  13. android 移除泛型中元素_最新(2020)Android高级面试知识点干货分享(二)
  14. 读取tomcat下的文件夹路径
  15. python开发的网络调试助手_Linux/windows/mac 下的socket网络通信调试助手 UDP/TCP
  16. 深入理解Android网络编程 技术详解与最佳实践电子书pdf下载
  17. python写入文件出现空行
  18. abb机器人焊接编程视频教程_【ABB】ABB机器人焊接指令介绍,内附视频
  19. 使用Javascript动态添加和删除元素
  20. 土地一分用计算机怎么算,土地计量单位换算(土地计量亩分厘单位)

热门文章

  1. 若人工智能研发是登山,我们都经历了什么
  2. echarts 日历热度图设置 calendar
  3. 浮动布局解决文字环绕图片问题
  4. windows操作系统
  5. 华东种业第一展——2014济南种子展的热闹和门道
  6. NPN型输入(输出)和PNP型输入(输出)分别是什么意思?
  7. Golang易错知识点
  8. 全志A33开发板vstart的屏幕与触摸屏旋转
  9. android实现浮动屏保,Android 屏保实现
  10. 对创业者总担心大公司抄袭的八点建议——兼谈腾讯微博