线程池(thread pool)在java的juc包下,new 方式初始化的线程池对应ThreadPoolExecutor这个类,可以在jdk源码中查看一下这个类,以及看一下线程池对应的每个参数的含义:

在jdk源码中可以看到创建线程池可以传入7个参数值,接下来对各个参数值做一个介绍

线程池各个参数含义
参数名 含义
corePoolSize 核心线程数
maxPoolSize 最大线程数
keepAliveTime 空闲线程的存活时间
unit 时间单位(如hour,minute,second等)
threadFactory 线程工厂,用来创建新的线程
workQueue 存放任务的队列
handler 处理被拒绝的任务(传入拒绝策略)

第一个参数核心线程数,当线程池中的线程工厂开始创建线程时,会先创建好指定数量的核心线程数,先由核心线程来处理任务,当核心线程全部在工作的时候,但是又有新的任务提交需要处理的时候,新的提交任务由于没有核心线程来处理新提交的任务,这时,新的任务就会被放在任务队列中,当有空闲的核心线程时,再从任务队列中取任务进行处理,当所有核心线程都在工作,同时新的任务又大量激增,塞满了任务队列时候,这时候,线程工厂就开始再创建新的线程,直到达到最大线程数,此时,就有最大线程数量的线程用来处理任务了,如果任务还在不断激增,同时增长的速度超过了所有线程处理的速度,那么就会很快,新的任务又塞满了任务队列,但是此时已经无法再创建新的线程来处理分担任务了,因为创建的线程已经达到了指定的最大数量,此时,就会调用拒绝策略,对继续提交的任务进行对应的拒绝策略。

当任务处理完后,线程就会处于空闲状态,但是不能一直让线程处于空闲状态,这样会造成资源浪费,因此需要设置空闲线程的存活时间,同时设置存活时间的时间单位,默认是不销毁核心线程的,但是可以通过设置将非核心行程中的空闲线程和核心线程的空闲线程全都销毁。

接下来介绍一下常见的线程池

FixedThreadPool

CachedThreadPool

ScheduledThreadPool

SingleThreadExecutor

SingleThreadScheduledExecutor

ForkJoinPool(java 8中新增)

第一种,看名字可以知道是固定线程池,就是核心线程数和最大线程数是一样的,始终创建一个指定的固定线程数量,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了

可以通过Executors类创建

创建方法如下:

 System.out.println(Runtime.getRuntime().availableProcessors());ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4*20);for (int i = 0; i < 10; i++) {final int index = i;fixedThreadPool.execute(new Runnable() {@Overridepublic void run() {try {System.out.println(Thread.currentThread().getName()+">>"+index);Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});}

使用Executors这个工厂类创建了一个固定线程数为80的线程池。接下来看一下源码:

可以看到源码中,通过传入的线程数量,最终其实还是通过ThreadPoolExecutor实例化线程池,不过这里只有5个参数,没有传入threadFactory和handler(拒绝策略),keepAliveTime设置为0表示永不超时;但是我们再到ThreadPoolExecutor类中看实例化方法源码

可以看到threadFactory用了一个默认的,handler也是默认的。

第二种线程池是 CachedThreadPool,可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

当我们提交一个任务后,线程池会判断已创建的线程中是否有空闲线程,如果有空闲线程则将任务直接指派给空闲线程,如果没有空闲线程,则新建线程去执行任务,这样就做到了动态地新增线程。让我们举个例子,如下方代码所示

ExecutorService service = Executors.newCachedThreadPool();for (int i = 0; i < 1000; i++) { service.execute(new Task() { });}

接下来再看一下newCacheThreadPool()方法的源码

可以看到核心线程为0,最大线程为Integer的最大值 。

根据参数值,可以解释一下上边那个for循环中执行的任务:使用 for 循环提交 1000 个任务给 CachedThreadPool,假设这些任务处理的时间非常长,会发生什么情况呢?因为 for 循环提交任务的操作是非常快的,但执行任务却比较耗时,就可能导致 1000 个任务都提交完了但第一个任务还没有被执行完,所以此时 CachedThreadPool 就可以动态的伸缩线程数量,随着任务的提交,不停地创建 1000 个线程来执行任务,而当任务执行完之后,假设没有新的任务了,那么大量的闲置线程又会造成内存资源的浪费,这时线程池就会检测线程在 60 秒内有没有可执行任务,如果没有就会被销毁,最终线程数量会减为 0

第三种线程池是 ScheduledThreadPool,它支持定时或周期性执行任务。比如每隔 10 秒钟执行一次任务,

创建该线程池源码:

调用了ScheduledThreadPoolExecutor实例化

而实现这种功能的方法主要有 3 种,如代码所示:

ScheduledExecutorService service = Executors.newScheduledThreadPool(10);service.schedule(new Task(), 10, TimeUnit.SECONDS);service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);

那么这 3 种方法有什么区别呢?

  • 第一种方法 schedule 比较简单,表示延迟指定时间后执行一次任务,如果代码中设置参数为 10 秒,也就是 10 秒后执行一次任务后就结束。

  • 第二种方法 scheduleAtFixedRate 表示以固定的频率执行任务,它的第二个参数 initialDelay 表示第一次延迟时间,第三个参数 period 表示周期,也就是刚开始时候一个延迟时间后开始执行任务,然后每次间隔多长时间执行一次任务(也就是延迟10分钟之后开始执行任务,然后就每隔第三个参数的时间执行一次任务)。

  • 第三种方法 scheduleWithFixedDelay 与第二种方法类似,也是周期执行任务,区别在于对周期的定义,之前的 scheduleAtFixedRate 是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行;而 scheduleWithFixedDelay 方法以任务结束的时间为下一次循环的时间起点开始计时。

举例说明采用第二种scheduleAtFixedRate方法:比如上班时候喝水,按照上午9点上班,每半个小时去喝一次水,喝一次水5分钟

9:00去接水;9:05喝完水

9:30去接水;9:35喝完水

10:00去接水;10:05喝完水

如果采用第三种方法scheduleWithFixedDelay,那么

9:00去接水;9:05喝完水

9:35去接水;9:40喝完水

10:10去接水;10:15喝完水

至此可以看出两种方法的差别了,第二种方法是不管你喝水花了多久,都是按照接水开始的时间开始算,每半个小时去接水,喝水,是一个固定频率;

而第三种的时间间隔,是以喝完水(完成任务)的时间为下次任务间隔开始的起点往后延迟30分钟。

第四种线程池是 SingleThreadExecutor,它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。源码如下:

第五个线程池是 SingleThreadScheduledExecutor,它实际和第三种 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程,如源码所示

接下来总结一下这五种常见线程池的特点

线程池参数说明
线程池类型 FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreadExecutor SingleThreadScheduledExector
corePoolSize 构造函数传入 0 构造函数传入 1 1
maxPoolSize 同corePoolSize Integer.MAX_VALUE Integer.MAX_VALUE 1 Integer.MAX_VALUE
keepAliveTime 0 60秒 0 0 0
workQueue LinkedBlockingQueue SynchronousQueue DelayedWorkQueue LinkedBlockingQueue DelayedWorkQueue
handler AbortPolicy(默认拒绝策略) AbortPolicy(默认拒绝略) AbortPolicy(默认拒绝略) AbortPolicy(默认拒绝策略) AbortPolicy(默认拒绝策略)

可以看到 5 种线程池对应了 3 种阻塞队列

LinkedBlockingQueue

对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

SynchronousQueue

第二种阻塞队列是 SynchronousQueue,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

DelayedWorkQueue

第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

使用Executors创建的这几种线程池,拒绝策略用的都是默认的AbortPolicy,不过线程任务拒绝策略有4种

当通过ThreadPoolExecutor初始化时候,可以指定任务拒绝策略:

Java 在 ThreadPoolExecutor 类中为我们提供了 4 种拒绝策略来应对不同的场景,都实现了 RejectedExecutionHandler 接口

  • 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
  • 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
  • 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
  • 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
    • 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
    • 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

通过上边的介绍可以看出,通过Executors这个类可以快速创建线程池(可以称为自动创建线程池),但是却不推荐这样创建线程池,这样做是有风险的

比如使用Executors.newCachedThreadPool()。这种方法创建的线程池不用传入参数,内部会调用new ThreadPoolexcecutor()方法,同时传入一些默认值,这样是有风险的,这样创建的核心线程数是0,最大的线程数是Integer的最大值(可以看做是无限多),同时使用的队列时SynchronousQueue,这个队列的特点是不存储任务,size为0,只起到一个传递任务的作用,所以远远不断的任务多来并不保存到队列中,而是需要不断创建新的线程来执行任务,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。

还有Executors.newFixedThreadPool(int nThreads),这是固定数量线程的线程池,这种创建线程池会传入一个线程数量,创建一个核心线程和最大线程一样的线程池,同时队列使用的是linkedBlockingQueue,这个队列容量没有上限,当固定的线程全部创建好后,但是任务数量特别多的情况,这些线程无法应付过来,这些线程就会源源不断放入到任务队列中,由于这个队列容量没有上限,所以可以一直往里边存放任务,队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生 OOM ,也就是OutOfMemoryError,这几乎会影响到整个程序,会造成很严重的后果。

newSingleThreadExecutor 和 newFixedThreadPool 的原理是一样的,只不过把核心线程数和最大线程数都直接设置成了 1,但是任务队列仍是无界的 LinkedBlockingQueue,所以也会导致同样的问题,也就是当任务堆积时,可能会占用大量的内存并导致 OOM

第四种线程池 ScheduledThreadPool 和第五种线程池 SingleThreadScheduledExecutor 的原理是一样的,只不过创建ScheduledThreadPool 会调用new ScheduledThreadPoolExecutor(),ScheduledThreadPoolExecutor 是 ThreadPoolExecutor 的子类

它采用的任务队列是 DelayedWorkQueue,这是一个延迟队列,同时也是一个无界队列,所以和 LinkedBlockingQueue 一样,如果队列中存放过多的任务,就可能导致 OOM。

可以看到,这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险,同时这也是阿里巴巴规约中规定的,所以平时使用线程池的时候最好是手动new ThreadPoolExecutor(),传入各个参数来创建线程池

如果你的idea安装了阿里巴巴规约插件,或者其他的代码审计插件会有提示的,不推荐你直接new Thread()创建线程,也不推荐你用Executors类来创建线程池

可以看到,当我用Executors创建线程池的时候,代码规约插件会友好提示的,同时代码的下边有红色波浪线警告,点击more,可以看到插件推荐的代码方式:

插件给的例子还是很详细的,所以平时写代码还是很有必要安装这类的代码检查插件:如alibaba java coding guidelines或者SonerLint等。

多线程之线程池复习总结相关推荐

  1. Java的多线程和线程池的使用,你真的清楚了吗?

    Java的多线程和线程池的使用 多线程大大提高程序运行效率,我们在开发过程中经常会开启一个线程来执行一些费时的任务.开启一个线程有4种方式,在下面的文章我将详细的去讲解. 继承Thread 继承Thr ...

  2. 多线程之线程池-各个参数的含义- 阿里,美团,京东面试题目

    阿里的面试官问了个问题,如果corepollSize=10,MaxPollSize=20,如果来了25个线程 怎么办, 答案: 当一个任务通过execute(Runnable)方法欲添加到线程池时: ...

  3. Java多线程之线程池配置合理线程数

    Java多线程之线程池配置合理线程数 目录 代码查看公司服务器或阿里云是几核的 合理线程数配置之CPU密集型 合理线程数配置之IO密集型 1. 代码查看公司服务器或阿里云是几核的 要合理配置线程数首先 ...

  4. Java多线程之线程池的手写改造和拒绝策略

    Java多线程之线程池的手写改造和拒绝策略 目录 自定义线程池的使用 四种拒绝策略代码体现 1. 自定义线程池的使用 自定义线程池(拒绝策略默认AbortPolicy) public class My ...

  5. Java多线程之线程池7大参数、底层工作原理、拒绝策略详解

    Java多线程之线程池7大参数详解 目录 企业面试题 线程池7大参数源码 线程池7大参数详解 底层工作原理详解 线程池的4种拒绝策略理论简介 面试的坑:线程池实际中使用哪一个? 1. 企业面试题 蚂蚁 ...

  6. Java多线程之线程池详解

    Java多线程之线程池详解 目录: 线程池使用及优势 线程池3个常用方式 线程池7大参数深入介绍 线程池底层工作原理 1. 线程池使用及优势 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务 ...

  7. pool python 传参数_Python-爬虫-多线程、线程池模拟(urllib、requests、UserAgent、超时等)...

    接着之前的MonkeyLei:Python-爬取页面内容(涉及urllib.requests.UserAgent.Json等) 继续练习下多线程,线程池模拟.. 我想这样: 1. 创建一个线程池,线程 ...

  8. Qt多线程-QThreadPool线程池与QRunnable

    版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:Qt多线程-QThreadPool线程池与QRunnable     本文地址:http:// ...

  9. java 多线程使用线程池_Java多线程:如何开始使用线程

    java 多线程使用线程池 什么是线程? (What is a Thread?) A thread is a lightweight process. Any process can have mul ...

最新文章

  1. 图像处理之霍夫变换(直线检測算法)
  2. OpenAPI 规范 3.1.0 发布,赶紧来尝尝鲜!
  3. i7跑服务器系统,i7主机做服务器
  4. SpringMVC实现简单的图片上传DEMO
  5. numpy学习笔记(莫烦python)
  6. JWT (Json Web Token)教程
  7. shell中的>/dev/null 2>1(转载)
  8. 行列式、LGV、矩阵树学习笔记
  9. [Oracle整理]CASE-END
  10. bzoj 1095 捉迷藏
  11. html里fill怎么自定义,HTML canvas fill()用法及代码示例
  12. 启用nf_conntrack模块,避免table full dropping
  13. python软件-python 2.7官方版
  14. 主流搜索引擎Lucene,Solr,ElasticSearch
  15. dp合集 广场铺砖问题硬木地板
  16. BAT命令手动_自动启动和禁用服务
  17. BootstrapTable的使用教程
  18. 分级基金及套利策略:申购套利、赎回套利、低折套利
  19. 小鸡G4工程款 上手体验
  20. 【CV】FPN:用于目标检测的特征金字塔网络

热门文章

  1. PowerShell说“此系统上的脚本执行被禁用。”
  2. mysql json字符串_mysql如何截取一个json字符串?
  3. makefile中 = ,:=,+=有怎么区别?
  4. c语言开发独立游戏,游戏项目一:c语言实现三子棋
  5. oracle 数据库基础配置,Oracle数据库网络服务配置基础、SQL编程详解-Oracle
  6. AWT_Swing_JPasswordField密码框(Java)
  7. 【Qt开发】StyleSheet使用总结
  8. CSS 用户界面样式
  9. win10屏蔽自动更新方法
  10. 计算机网络 教学大纲 谢希仁,《计算机网络》教学大纲.doc