Java-线程-线程池

news/2024/10/2 14:32:14

0.背景

参考资料:Java线程池实现原理及其在美团业务中的实践

在 Java 早期,每次创建线程时,都要涉及到线程的创建、销毁以及资源管理,这对于系统的性能和资源利用率是一种浪费。

因此,Java 提供了线程池的概念,以提高线程的管理效率和性能。

  • 资源管理优化:传统的线程创建和销毁需要涉及系统资源的分配和释放,而频繁的线程创建和销毁会导致资源的浪费和性能下降。

    线程池通过维护一组可重用的线程来减少线程的创建和销毁次数,从而优化了系统资源的利用。

  • 线程复用:线程池可以维护一定数量的线程池,这些线程可以被重复利用来执行多个任务,而不必每次都创建新线程。

    这样可以避免频繁地创建和销毁线程,提高了系统的性能和效率。

  • 任务调度和管理:线程池可以对任务进行调度和管理,可以根据任务的优先级和类型来调度执行线程,提供了更加灵活和可控的任务执行方式。

  • 并发控制:线程池提供了一种方便的方式来控制并发执行的线程数量,可以限制同时执行的线程数量,从而避免因线程过多导致系统资源耗尽或性能下降的问题。

1.概念

1.1 关键词

记忆方法:核大存单队略厂(这里和大存单的队伍略长)

1.1.1 核心线程数 corePoolSize

  • 含义:指定线程池中保持的活动线程数。除非设置了 allowCoreThreadTimeOuttrue,否则这些线程不会被回收。
  • 作用:核心线程数决定了线程池中保持的最小活动线程数,即使线程处于空闲状态也不会被回收。

1.1.2 最大线程数 maximumPoolSize

  • 含义:指定线程池中允许创建的最大线程数。当任务队列已满且核心线程数已达到最大时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。
  • 作用:最大线程数决定了线程池中允许创建的最大线程数,用于处理任务队列中的任务。

1.1.3 空闲线程存活时间 keepAliveTime

  • 含义:指定空闲线程的存活时间,即当线程池中的线程处于空闲状态且空闲时间超过该值时,这些空闲线程可以被回收。
  • 作用:通过设置线程的存活时间,可以控制线程池中线程的数量,避免因线程过多导致系统资源的浪费。

1.1.4 空闲线程存活时间单位 unit

  • 含义:指定空闲线程存活时间的单位,可以是纳秒、微秒、毫秒、秒、分钟、小时或天等。
  • 作用:单位参数用于指定空闲线程存活时间的时间单位,与 keepAliveTime 参数配合使用。

1.1.5 工作队列 workQueue

  • 含义:任务队列,用于保存等待执行的任务。线程池中的线程会从任务队列中取出任务执行。
  • 作用:工作队列用于存储提交给线程池但尚未执行的任务,当线程池中的线程处于忙碌状态时,新的任务会被放入任务队列等待执行。

1.1.6 拒绝策略 rejectedExecutionHandler

  • 含义:拒绝策略,用于处理当任务无法被线程池执行时的情况。当任务队列已满且无法接受新的任务时,或者线程池已关闭但仍然有任务提交时,会触发拒绝策略。
  • 作用:拒绝策略定义了线程池无法处理任务时的行为,包括抛出异常、丢弃任务、阻塞调用者或者执行任务的线程等待一段时间后再尝试执行任务等。选择合适的拒绝策略能够有效地处理任务提交过载的情况,保护线程池和系统的稳定性。

1.1.7 线程工厂 threadFactory

  • 含义:线程工厂,用于创建新的线程。当线程池需要创建新线程时,会调用线程工厂的 newThread(Runnable r) 方法来创建线程。
  • 作用:线程工厂用于创建线程池中的线程实例,通过自定义线程工厂,可以控制线程的创建过程,如设置线程的名称、优先级、是否为守护线程等。

1.2 线程池的状态

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。

运行状态 是否接收任务 是否处理任务 状态描述
RUNNING 能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN × 不再接受新提交的任务,可以继续处理阻塞队列中已保存的任务。
STOP × × 不再接受新任务,也不继续处理队列中的任务,会中断正在处理任务的线程。
TIDYING - - 所有的任务都已终止了,workerCount(有效线程数)为0。
TERMINATED - - 终止状态,在 terminated() 方法(预留)执行完后进入该状态。

状态转换如下:

图3 线程池生命周期

1.3 运行过程

有核心线程,优先用核心线程。核心线程不够用,先往工作队列里放。工作队列都放不下了,再创建非核心线程来执行。实在是不行了,只能进行拒绝。

  • 开始,接收到提交的一个任务。

  • 判断线程池是否还在运行

    • 如果不是RUNNING状态,则不再接受任务提交,进行拒绝策略。

    • 如果是RUNNINNG状态,可以接收任务提交。

      判断当前工作线程数与核心线程数的关系

      • 小于核心线程数,新建工作线程(核心)并执行。

      • 大于等于核心线程数

        检查工作队列情况

        • 工作队列未满,添加任务到工作队列中,等待获取执行。

        • 工作队列已满

          检查最大线程数

          • 未达到最大线程数,新建工作线程(非核心)执行。
          • 已经达到最大线程数,进行任务拒绝。

图4 任务调度流程

1.4 理解

注意,以下是我的理解,我半桶水,你记得带自己的思考来阅读喔。

1.4.1 核心线程数和最大线程数

忽略核心线程是否可以销毁

核心线程必定在运行,是我们使用的最小成本。而最大线程是一个兜底机制,不可能无休止的来创建线程,必定要有个度。

线程池中的任务,是依赖核心线程和非核心线程来执行的,不是说跟线程创建销毁跟工作队列没关系,而是侧重点不同,工作队列的目的是缓冲。

1.4.2 工作队列存在的意义

工作队列的存在,是为了提供一个缓冲,帮助线程池保持一个最小可运转的线程数量,避免频繁的创建和销毁线程。

好,我们回顾上面的图,核心线程没满的时候,我们可以新建线程来执行,在核心线程内的数量,我们是很好接受的。

那么,当我的核心线程都不够用了,这个时候,暴力的方式当然是直接新建线程来执行。

  • 优点,核心线程在忙碌,我直接搂一个新线程,立马就能执行,效率很高。
  • 缺点,涉及到的频繁的创建线程(高成本)。

有了工作队列后,我们核心线程都在忙碌时,选择先将任务放到队列里等一等,实在再放不下了,再来创建线程执行。

我们还可以极端点,假设我们新建一个线程要2s,执行一个任务只要0.5s。

核心线程已满,这个时候,我们是不是等个0.5s就能空闲出一个核心线程,然后就有得执行了。而不是选择立马新建一个,耗费2s。

1.4.3 ThreadPoolExecutor分析

1.4.3.1 关于参数校验

经常会有面试问,核心线程数能设置为0吗?

注意这是构造方法,是做了校验的。

A.参数不合法异常 IllegalArgumentException
  • 核心线程数 < 0
  • 最大线程数 <= 0
  • 最大线程数 < 核心线程数
  • 线程存活时间小于0
B.空指针异常NullPointerException
  • 工作队列为null
  • 线程工厂为null
  • 拒绝策略为null
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}

这个构造函数,最少需要5个核心参数喔。

image-20240509154827473

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);}
1.4.3.2 核心线程数可以为0吗?

可以

线程池也可以接收任务,线程池会依赖非核心线程来执行任务。

任务首先会被放入工作队列进行缓冲,如果工作队列已满,再根据最大线程数的限制来新建非核心线程来执行任务。

只有在工作队列已满,且线程池中的线程数未达到最大线程数时,才会创建新的线程,如果线程池已达到最大线程数,则不会再创建新线程,根据拒绝策略来处理任务。

然后呢,这个时候也有个问题,由于我们没有核心线程,直接被放到工作队列里,这个队列没满的话,就不会创建线程,相当于在干等。

嗯,就跟我们写的代码一样,让人感到无语。

1.4.3.3 最大线程数可以为0吗?

不可以

首先通过前面可以知道,最大线程数必须比核心线程数大,然后呢,核心线程数不能小于0(参数异常),但是核心线程数可以等于0。

然后呢,还有个校验,是最大线程数是必须大于核心线程数的,所以说呀,这个最大线程数,最小值是1

哦,sorry,最大线程数的判断直接是maximumPoolSize <= 0 抛异常。

我没骗你,你自己看图。

image-20240509155047977

image-20240509155030262

1.4.4 执行流程代码简述

public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}else if (!addWorker(command, false))reject(command);
}

2.实现

2.1 Executor接口

  • 定义了一个单一的方法 execute(Runnable command),用于执行提交的任务。
  • 只关注任务的提交方式,不关心任务的执行细节。

image-20240507223239290

2.2 ExecutorService接口

  • Executor 接口的子接口,提供了更多的方法用于任务管理和线程池控制。
  • 定义了一系列方法来提交任务、关闭线程池、管理任务的执行状态等。

image-20240507223408199

2.3 Executors类

  • 是一个工具类,提供了一系列静态方法用于创建不同类型的线程池。
  • 通过 Executors 类可以快速方便地创建各种预定义类型的线程池,如固定大小线程池、可缓存线程池等

image-20240507223316563

2.4 ThreadPoolExecutor类

  • ExecutorService 接口的一个具体实现类,也是 Executors 类创建线程池的底层实现。
  • 提供了一个灵活的线程池实现,可以通过构造方法来设置各种参数,如核心线程数、最大线程数、任务队列、拒绝策略等。

image-20240507223451061

3.进阶

3.1 线程池中的异常

3.2 自定义拒绝策略

3.3 获取任务执行结果

3.3.1 Future

3.3.2 Callable

3.4 定时任务和周期性任务的调度和执行

3.4.1 ScheduledExecutorService

3.4.2 ScheduledThreadPoolExecutor

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/29396.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

8.2版本Web端移动开发调试强制跳转新移动框架

解决方案: Common.config文件中增加配置项 <add key="MobileLoginType" value="1" /> 如下图其他注意事项: 没有配置MobileLoginType属性 或 MobileLoginType = "" 或 MobileLoginType = 2 都会执行重定向 MobileLoginType = 3 系…

Error: Cannot find module ‘D:\SoftSetupLoaction\nodejs\node_global\node_modules\npm\bin\npm-cli.js‘

Error: Cannot find module ‘D:\SoftSetupLoaction\nodejs\node_global\node_modules\npm\bin\npm-cli.js‘ 出现原因: 重新安装可装了nodejs和npm 网上查了很多方法,都建议重装,但是都没有效果(因为我就是重装之后出现的问题) 按照错误提示node_global找不到npm-cli.js,个人…

初探pinctrl子系统和GPIO子系统

前言: 在前面的led驱动程序和按键驱动程序中,无论是最传统的方法,还是总线设备驱动模型,还是基于设备树的总线设备驱动模型,都是直接操作寄存器的方法。驱动开发的本质确实是操作寄存器,但是一个芯片有几百个引脚,只是操作少数的几个引脚还好,如果是大量的引脚,比如LC…

PVE新增硬盘并扩容给 local分区

PVE安装在120G的固态硬盘,现在加了一块1T的机械硬盘作为虚拟机系统用,需要把磁盘扩容给 local分区 1、ssh连上pve,使用 lsblk 查看硬盘驱动器路径,我这里新加的硬盘是 sda,硬盘还未进行分区 2、fdisk /dev/sda,对硬盘进行分区操作,注意你自己的硬盘名称,千万小心不要搞…

《最新出炉》系列入门篇-Python+Playwright自动化测试-45-鼠标操作-下篇

1.简介 鼠标为我们使用电脑提供了很多方便,我们看到的东西就可以将鼠标移动过去进行点击就可以打开或者访问内容,当页面内容过长时,我们也可以使用鼠标滚轮来实现对整个页面内容的查看,其实playwright也有鼠标操作的方法。上一篇文章中已经讲解过鼠标的部分操作了,今天宏哥…

redux中核心组件有哪些,reducer的作用

在redux中,核心组件包括Action、Reducer、Store和Middleware。 Action是一个普通的JavaScript对象,用于描述发生了什么事件。它必须包含一个type属性,用于标识事件的类型。可以在Action中添加其他自定义的属性来传递数据。 Reducer是一个纯函数,用于根据收到的Action来更新…

学习记录+vcode+GPIO例程+正点原子 DNESP32S3 开发板教程-IDF 版

第一个程序: UART模式和JTAG模式,配置完成不同。配置主要就是.vscode 文件夹中 c_cpp_properties.json,tasks.json,launch.json,settings.json四个文件。 一个想法:备份UART模式和JTAG模式的配置文件,用时直接文件替换。简单粗暴方式是.vscode 文件夹替换。当然每次要选…

AB实验相关流程

本篇文章介绍的是一个完整AB测试流程应该怎么走。 AB测试流程有以下几个步骤: 一、选取实验指标 二、建立实验假设 三、选取实验单位 四、确定最小提升预期值 五、计算最小样本量 六、流量分割 七、确定实验时长 八、数据统计 九、得出结论 接下来就详细说明每个步骤。 一、选…