当前位置:网站首页 > 技术博客 > 正文

线程池的理解及使用



每天早上七点三十,准时推送干货

虽然 Java 对线程的创建、中断、等待、通知、销毁、同步等功能提供了很多的支持,但是从操作系统角度来说,频繁的创建线程和销毁线程,其实是需要大量的时间和资源的。

例如,当有多个任务同时需要处理的时候,一个任务对应一个线程来执行,以此来提升任务的执行效率,模型图如下:

如果任务数非常少,这种模式倒问题不大,但是如果任务数非常的多,可能就会存在很大的问题:

假如把很多任务让一组线程来执行,而不是一个任务对应一个新线程,这种通过接受任务并进行分发处理的就是线程池

线程池内部维护了若干个线程,当没有任务的时候,这些线程都处于等待状态;当有新的任务进来时,就分配一个空闲线程执行;当所有线程都处于忙碌状态时,新任务要么放入队列中等待,要么增加一个新线程进行处理,要么直接拒绝。

很显然,这种通过线程池来执行多任务的思路,优势明显:

关于这一点,我们可以看一个简单的对比示例。

两者执行耗时情况对比,如下:

从结果上可以看出,同样的任务数,采用线程池和不采用线程池,执行耗时差距非常明显,一个任务对应一个新的线程来执行,反而效率不如采用 4 个线程的线程池执行的快。

为什么会产生这种现象,下面我们就一起来聊聊线程池。

站在专业的角度讲,线程池其实是一种利用池化思想来实现线程管理的技术,它将线程的创建和任务的执行进行解耦,同时复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。通过合理的参数设置,可以实现更低的系统资源使用率、更高的任务并发执行效率。

在 Java 中,线程池最顶级的接口是,名下的实现类关系图如下:

关键接口和实现类,相关的描述如下:

整个关系图中,其中是线程池最核心的实现类,开发者可以使用它来创建线程池。

2.1、ThreadPoolExecutor 构造方法

类的完整构造方法一共有七个参数,理解这些参数的配置对使用好线程池至关重要,完整的构造方法核心源码如下:

各个参数的解读如下:

2.2、ThreadPoolExecutor 执行流程

创建完线程池之后就可以提交任务了,当有新的任务进来时,线程池就会工作并分配线程去执行任务。

的典型用法如下:

针对任务的提交方式,还提供了两种方法。

执行提交的任务流程虽然比较复杂,但是通过对源码的分析,大致的任务执行流程,可以用如下图来概括。

整个执行流程,大体步骤如下:

我们再回头来看上文提到的构造方法中的七个参数,这些参数会直接影响线程的执行情况,各个参数的变化情况,可以用如下几点来概括:

执行任务的部分核心源码如下!

2.2.1、execute 提交任务
2.2.2、addWorker 创建线程加入线程池
2.2.3、runWorker 执行任务
2.2.4、reject 执行拒绝策略

当线程池中的线程数大于等于 maximumPoolSize,并且 workQueue 已满,新任务会被拒绝,使用接口的方法来处理被拒绝的任务。

线程池提供了四种拒绝策略实现类来拒绝任务,具体如下:

2.3、ThreadPoolExecutor 线程池状态

我们知道 Java 种的线程一共 6 种状态,其实线程池也有状态。

因为线程池也是异步执行的,有的任务正在执行,有的任务存储在任务队列中,有的线程处于工作状态,有的线程处于空闲状态等待回收,为了更加精细化的管理线程池,线程池也设计了 5 中状态,部分核心源码如下:

其中的状态流程,可以用如下图来描述!

这几个状态的转化关系,可以用如下几个步骤来概括:

正如文章的开头所介绍的,使用线程池的方式,通常可以用如下几个步骤来概括:

正如上文所说,其中和方法都可以用来提交任务,稍有不同的是:方法同时还支持获取任务执行完毕的返回结果。

针对线程池的使用,Java 还提供了工具类,开发者可以通过此工具,快速创建不同类型的线程池。

下面我们一起来看下为用户提供的几种创建线程池的方法。

3.1、newSingleThreadExecutor

方法表示创建一个单线程的线程池,核心源码如下:

从构造参数上可以很清晰的看到,线程池中的线程数为 1,不会被线程池自动回收,workQueue 选择的是无界的阻塞队列,不管来多少任务存入阻塞队列中,前面一个任务执行完毕,再执行队列中的剩余任务。

简单应用示例如下:

运行结果如下:

3.2、newFixedThreadPool

方法表示创建一个固定大小线程数的线程池,核心源码如下:

固定大小的线程池和单线程的线程池有异曲同工之处,无非是让线程池中能运行的线程数量支持手动指定。

简单应用示例如下:

运行结果如下:

3.3、newCachedThreadPool

方法表示创建一个可缓存的无界线程池,核心源码如下:

从构造参数上可以看出,线程池中的最大线程数为,也就是的最大值,workQueue 选择的是阻塞队列,这个阻塞队列不像,它没有容量,只负责做临时任务缓存,如果有任务进来立刻会被执行。

也就是说,只要添加进去了任务,线程就会立刻去执行,当任务超过线程池的线程数则创建新的线程去执行,线程数量的最大上线为,当线程池中的线程空闲时间超过 60s,则会自动回收该线程。

简单应用示例如下:

运行结果如下:

3.4、newScheduledThreadPool

方法表示创建周期性的线程池,可以指定线程池中的核心线程数,支持定时及周期性任务的执行,核心源码如下:

从构造参数上可以看出,线程池支持指定核心线程数,最大线程数为,workQueue 选择的是延迟阻塞队列,这个阻塞队列支持任务延迟消费,新加入的任务不会立刻被执行,只有时间到期之后才会被取出;当非核心线程处于空闲状态时,会立刻进行收回。

支持三种类型的定时调度方法,分别如下:

下面我们一起来看看它们的应用方式。

3.4.1、schedule 方法使用示例

输出结果:

3.4.2、scheduleAtFixedRate 方法使用示例

输出结果:

3.4.3、scheduleWithFixedDelay 方法使用示例

输出结果:

3.5、工厂方法小结

从以上的介绍中,我们可以对这四种线程池的参数做一个汇总,内容如下表:

这四个线程池,主要的区别在于:corePoolSize、maximumPoolSize、keepAliveTime、workQueue 这四个参数,其中线程工厂为默认类,线程饱和的拒绝策略为默认类。

结合以上的分析,最后我们再来总结一下。

对于线程池的使用,不太建议采用工具去创建,尽量通过的构造方法来创建,原因在于:有利于规避资源耗尽的风险;同时建议开发者手动设定任务队列的上限,防止服务出现 OOM。

虽然工具提供了四种创建线程池的方法,能帮助开发者省去繁琐的参数配置,但是和方法创建的线程池,任务队列上限为,这意味着可以无限提交任务,这在高并发的环境下,系统可能会出现 OOM,导致整个线程池不可用;其次方法也存在同样的问题,无限的创建线程可能会给系统带来更多的资源消耗。

其次,创建线程池的时候应该尽量给线程定义一个具体的业务名字前缀,方便定位问题,不同类型的业务尽量使用不同的线程池来实现。

例如可以使用包,创建自定义的线程工厂。

当然,你也可以自行实现一个线程工厂,需要继承接口,案例如下:

创建一个线程名称以开头的线程工厂。

最后,再来说说关于线程池中线程数,如何合理设定的问题?

那如何判断当前是 CPU 密集型任务还是 I/O 密集型任务呢?

最简单的方法就是:如果当前任务涉及到网络读取,文件读取等,这类都是 IO 密集型任务,除此之外,可以看成是 CPU 密集型任务。

本文篇幅比较长,难免有描述不对的地方,欢迎大家留言指出!

喜欢就分享

认同就点赞

支持就在看

一键四连,你的offer也四连

  • 上一篇: 结构体(例题详解)
  • 下一篇: java的工具
  • 版权声明


    相关文章:

  • 结构体(例题详解)2025-09-07 22:01:02
  • 进程和线程的基本概念及两者之间的区别?2025-09-07 22:01:02
  • sql触发器的使用及语法2025-09-07 22:01:02
  • 扩散系数d的单位怎么来的2025-09-07 22:01:02
  • javajdk哪个版本好2025-09-07 22:01:02
  • java的工具2025-09-07 22:01:02
  • crossorigin注解原理2025-09-07 22:01:02
  • linux自带的虚拟机2025-09-07 22:01:02
  • 电脑端小说阅读器哪个好2025-09-07 22:01:02
  • c++cin.getline函数2025-09-07 22:01:02