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

线程池用法 java



  • 一 使用线程池的好处
  • 二 Executor 框架
    • 2.1 简介
    • 2.2 Executor 框架结构(主要由三大部分组成)
      • 1) 任务( /)
      • 2) 任务的执行()
      • 3) 异步计算的结果()
    • 2.3 Executor 框架的使用示意图
  • 三 (重要)ThreadPoolExecutor 类简单介绍
    • 3.1 ThreadPoolExecutor 类分析
    • 3.2 推荐使用 构造函数创建线程池
  • 四 (重要)ThreadPoolExecutor 使用示例
    • 4.1 示例代码:+
    • 4.2 线程池原理分析
    • 4.3 几个常见的对比
      • 4.3.1 vs
      • 4.3.2 vs
      • 4.3.3 VS
      • 4.3.2 VS
    • 4.4 加餐:+示例代码
  • 五 几种常见的线程池详解
    • 5.1 FixedThreadPool
      • 5.1.1 介绍
      • 5.1.2 执行任务过程介绍
      • 5.1.3 为什么不推荐使用?
    • 5.2 SingleThreadExecutor 详解
      • 5.2.1 介绍
      • 5.2.2 执行任务过程介绍
      • 5.2.3 为什么不推荐使用?
    • 5.3 CachedThreadPool 详解
      • 5.3.1 介绍
      • 5.3.2 执行任务过程介绍
      • 5.3.3 为什么不推荐使用?
  • 六 ScheduledThreadPoolExecutor 详解
    • 6.1 简介
    • 6.2 运行机制
    • 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤
  • 七 线程池大小确定

池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.1 简介

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

2.2 Executor 框架结构(主要由三大部分组成)

1) 任务( /)

执行任务需要实现的 接口接口 接口 接口 实现类都可以被 执行。

2) 任务的执行()

如下图所示,包括任务执行机制的核心接口 ,以及继承自 接口的 接口。 这两个关键类实现了 ExecutorService 接口

这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

注意: 通过查看 源代码我们发现 实际上是继承了 并实现了 ScheduledExecutorService ,而 又实现了 ,正如我们下面给出的类关系图显示的一样。

类描述:

 

类描述:

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v8EHagpd-49)(/images/java线程池学习总结/任务的执行相关接口.png)]

3) 异步计算的结果()

接口以及 接口的实现类 类都可以代表异步计算的结果。

当我们把 接口 接口 的实现类提交给 执行。(调用 方法时会返回一个 对象)

2.3 Executor 框架的使用示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uy5SejjZ-50)(/images/java线程池学习总结/Executor框架的使用示意图.png)]

  1. 主线程首先要创建实现 或者 接口的任务对象。
  2. 把创建完成的实现 /接口的 对象直接交给 执行: )或者也可以把 对象或 对象提交给 执行(或 )。
  3. 如果执行 , 将返回一个实现接口的对象(我们刚刚也提到过了执行 方法和 方法的区别,会返回一个 实现了 ,我们也可以创建 ,然后直接交给 执行。
  4. 最后,主线程可以执行 方法来等待任务执行完成。主线程也可以执行 来取消此任务的执行。

线程池实现类 是 框架最核心的类。

3.1 ThreadPoolExecutor 类分析

类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

 

下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。

3 个最重要的参数:

  • : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

其他常见参数:

  1. :当线程池中的线程数量大于 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 才会被回收销毁;
  2. : 参数的时间单位。
  3. :executor 创建新线程的时候会用到。
  4. :饱和策略。关于饱和策略下面单独介绍一下。

下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KX62Whcm-51)(/images/java线程池学习总结/线程池各个参数之间的关系.png)]

饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时, 定义一些策略:

  • :抛出 来拒绝新任务的处理。
  • :调用执行自己的线程运行任务,也就是直接在调用方法的线程中运行()被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • 不处理新任务,直接丢弃掉。
  • 此策略将丢弃最早的未处理的任务请求。

举个例子:

Spring 通过 或者我们直接通过 的构造函数创建线程池的时候,当我们不指定 饱和策略的话来配置线程池的时候默认使用的是 。在默认情况下, 将抛出 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)

3.2 推荐使用 构造函数创建线程池

在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

为什么呢?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

方式一:通过构造函数实现(推荐)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APRfF2oE-53)(/images/java线程池学习总结/threadpoolexecutor构造函数.png)]
方式二:通过 Executor 框架的工具类 Executors 来实现
我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

我们上面讲解了 框架以及 类,下面让我们实战一下,来通过写一个 的小 Demo 来回顾上面的内容。

4.1 示例代码:+

首先创建一个 接口的实现类(当然也可以是 接口,我们上面也说了两者的区别。)

 

编写测试程序,我们这里以阿里巴巴推荐的使用 构造函数自定义参数的方式来创建线程池。

 

可以看到我们上面的代码指定了:

  1. : 核心线程数为 5。
  2. :最大线程数 10
  3. : 等待时间为 1L。
  4. : 等待时间的单位为 TimeUnit.SECONDS。
  5. :任务队列为 ,并且容量为 100;
  6. :饱和策略为 。

Output:

 

4.2 线程池原理分析

承接 4.1 节,我们通过代码输出结果可以看出:线程首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)

现在,我们就分析上面的输出内容来简单分析一下线程池原理。

为了搞懂线程池的原理,我们需要首先分析一下 方法。 在 4.1 节中的 Demo 中我们使用 来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:

 

通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WehvOHm4-55)(/images/java线程池学习总结/图解线程池实现原理.png)]

这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。

 

更多关于线程池源码分析的内容推荐这篇文章:《JUC线程池ThreadPoolExecutor源码分析》

现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?

没搞懂的话,也没关系,可以看看我的分析:

我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。

4.3 几个常见的对比

4.3.1 vs

自 Java 1.0 以来一直存在,但仅在 Java 1.5 中引入,目的就是为了来处理不支持的用例。 接口不会返回结果或抛出检查异常,但是 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 接口,这样代码看起来会更加简洁。

工具类 可以实现 对象和 对象之间的相互转换。()或 )。

 

 
4.3.2 vs
  1. 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. 方法用于提交需要返回值的任务。线程池会返回一个 类型的对象,通过这个 对象可以判断任务是否执行成功 ,并且可以通过 的 方法来获取返回值,方法会阻塞当前线程直到任务完成,而使用 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

我们以 接口中的一个 方法为例子来看看源代码:

 

上面方法调用的 方法返回了一个 对象。

 

我们再来看看方法:

 
4.3.3 VS
  • :关闭线程池,线程池的状态变为 。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • :关闭线程池,线程的状态变为 。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
4.3.2 VS
  • 当调用 方法后返回为 true。
  • 当调用 方法后,并且所有提交的任务完成后返回为 true

4.4 加餐:+示例代码

 

 

Output:

 

5.1 FixedThreadPool

5.1.1 介绍

被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:

 

另外还有一个 的实现方法,和上面的类似,所以这里不多做阐述:

 

从上面源代码可以看出新创建的 的 和 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。

5.1.2 执行任务过程介绍

的 方法运行示意图(该图片来源:《Java 并发编程的艺术》):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ivs8Rwb-56)(/images/java线程池学习总结/FixedThreadPool.png)]

上图说明:

  1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 ;
  3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 中获取任务来执行;
5.1.3 为什么不推荐使用?

使用无界队列 (队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :

  1. 当线程池中的线程数达到 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
  2. 由于使用无界队列时 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 的源码可以看出创建的 的 和 被设置为同一个值。
  3. 由于 1 和 2,使用无界队列时 将是一个无效参数;
  4. 运行中的 (未执行 或 )不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

5.2 SingleThreadExecutor 详解

5.2.1 介绍

是只有一个线程的线程池。下面看看SingleThreadExecutor 的实现:

 
 

从上面源代码可以看出新创建的 的 和 都被设置为 1.其他参数和 相同。

5.2.2 执行任务过程介绍

的运行示意图(该图片来源:《Java 并发编程的艺术》):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2myW5kHD-56)(/images/java线程池学习总结/SingleThreadExecutor.png)]

上图说明;

  1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
  2. 当前线程池中有一个运行的线程后,将任务加入
  3. 线程执行完当前的任务后,会在循环中反复从 中获取任务来执行;
5.2.3 为什么不推荐使用?

使用无界队列 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。 使用无界队列作为线程池的工作队列会对线程池带来的影响与 相同。说简单点就是可能会导致 OOM,

5.3 CachedThreadPool 详解

5.3.1 介绍

是一个会根据需要创建新线程的线程池。下面通过源码来看看 的实现:

 
 

的 被设置为空(0),被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 中线程处理任务的速度时, 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

5.3.2 执行任务过程介绍

CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NDbzlpq2-57)(/images/java线程池学习总结/CachedThreadPool-execute.png)]

上图说明:

  1. 首先执行 提交任务到任务队列。如果当前 中有闲线程正在执行 ,那么主线程执行 offer 操作与空闲线程执行的 操作配对成功,主线程把任务交给空闲线程执行,方法执行完成,否则执行下面的步骤 2;
  2. 当初始 为空,或者 中没有空闲线程时,将没有线程执行 。这种情况下,步骤 1 将失败,此时 会创建新线程执行任务,execute 方法执行完成;
5.3.3 为什么不推荐使用?

允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目中基本不会被用到,因为有其他方案选择比如。大家只需要简单了解一下它的思想。关于如何在 Spring Boot 中 实现定时任务,可以查看这篇文章《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》。

6.1 简介

使用的任务队列 封装了一个 , 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行( 的 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行( 的 变量小的先执行)。

和 的比较:

  • 对系统时钟的变化敏感,不是;
  • 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程;
  • 在 中抛出的运行时异常会杀死一个线程,从而导致 死机:-( …即计划任务将不再运行。 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 方法)。抛出异常的任务将被取消,但其他任务将继续运行。

综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。

备注: Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。

6.2 运行机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4JW9p3CC-57)(/images/java线程池学习总结/ScheduledThreadPoolExecutor机制.png)]

的执行主要分为两大部分:

  1. 当调用 的 方法或者 方法时,会向 的 添加一个实现了 接口的
  2. 线程池中的线程从 中获取 ,然后执行任务。

为了实现周期性的执行任务,对 做了如下修改:

  • 使用 作为任务队列;
  • 获取任务的方不同
  • 执行周期任务后,增加了额外的处理

6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rrk3MpKF-58)(images/java线程池学习总结/ScheduledThreadPoolExecutor执行周期任务步骤.png)]

  1. 线程 1 从 中获取已到期的 。到期任务是指 的 time 大于等于当前系统的时间;
  2. 线程 1 执行这个 ;
  3. 线程 1 修改 的 time 变量为下次将要被执行的时间;
  4. 线程 1 把这个修改 time 之后的 放回 中()。

线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。

很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

版权声明


相关文章:

  • json 字符集2025-01-18 10:01:03
  • 运维开发需要懂什么2025-01-18 10:01:03
  • 2020年最新一期的编程语言排行榜2025-01-18 10:01:03
  • pyhton如何安装2025-01-18 10:01:03
  • 结构体数组定义和使用2025-01-18 10:01:03
  • 数字图像处理实验总结2025-01-18 10:01:03
  • 应用层的基本协议主要有2025-01-18 10:01:03
  • visual studio code创建python项目2025-01-18 10:01:03
  • python common库2025-01-18 10:01:03
  • 面向对象设计主要设计哪些基本概念2025-01-18 10:01:03