Java线程池最全详解

1. 引言

在当今高度并发的软件开发环境中,有效地管理线程是确保程序性能和稳定性的关键因素之一。Java线程池作为一种强大的并发工具,不仅能够提高任务执行的效率,还能有效地控制系统资源的使用。
本文将深入探讨Java线程池的原理、参数配置、自定义以及实际应用。通过理解这些关键概念,开发者将能够更好地应对不同的并发场景,优化程序的执行效率。
首先,我们将介绍线程池的基本概念,解释它在并发编程中的作用和优势。随后,我们将深入研究Java线程池的工作原理,剖析其在任务提交、执行和线程管理方面的内部机制。

2. Java线程池的基础概念

在并发编程中,线程池是一种重要的设计模式,它能够有效地管理和复用线程,提高程序的性能和资源利用率。Java线程池作为Java并发包(java.util.concurrent)的一部分,为开发者提供了方便、高效的多线程处理方式。同时在阿里巴巴开发规范中,强制要使用线程池去提供线程,不允许在代码中显示的创建线程。

2.1 什么是线程池?

线程池是由一组线程组成的线程队列,它们在程序启动时就被创建并一直存在。这些线程可被用来执行提交到线程池的各种任务,从而避免为每个任务都创建新线程。这种机制能够降低线程创建和销毁的开销,提高系统性能。

2.2 线程池的工作原理

线程池的工作原理基于任务队列和线程管理机制。当任务被提交到线程池时,线程池会选择合适的线程来执行任务。如果核心线程数未达到上限,新任务可能会导致新线程的创建。如果核心线程已满,任务将被放入任务队列等待执行。当任务队列也已满,而同时线程数未达到最大线程数,新的任务将创建临时线程来执行。

2.3 线程池的优势

使用线程池的优势主要体现在以下几个方面:
减少资源消耗: 线程的创建和销毁是有开销的,线程池通过复用线程,减少了这些开销。
提高响应速度: 由于线程池中的线程一直存在,可以更迅速地响应任务的到来。
避免系统过载: 控制线程数量,防止系统因过多线程而过载。

3. Java线程池的工作原理

Java线程池的工作原理涉及线程的创建、任务的提交与执行,以及对线程的管理。深入理解这些机制对于优化并发程序至关重要。

3.1 线程池的创建与初始化

在程序启动时,线程池被创建并初始化。这一过程包括设置线程池的基本参数,如核心线程数、最大线程数、任务队列等。核心线程数是线程池中一直存活的线程数量,而最大线程数则是线程池允许创建的最大线程数量。例如创建一个固定核心线程数的线程:

ExecutorService executorService = Executors.newFixedThreadPool(corePoolSize);

其中参数corePoolSize即为核心线程数

3.2 任务的提交与执行

任务提交到线程池后,线程池会根据一定的策略选择线程来执行任务。首先,线程池会检查核心线程是否已满,如果未满,新的任务可能会导致新线程的创建。如果核心线程已满,任务将被放入任务队列。
在Java线程池中,任务的提交与执行有两个主要的方法:submitexecute。这两种方法有一些区别,主要体现在返回值、异常处理和任务包装上。

3.2.1 submit方法

submit方法用于提交实现了Callable接口的任务,它可以返回一个Future对象,通过该对象可以获取任务执行的结果,取消任务等。submit方法还可以接受实现了Runnable接口的任务,但它无法获取任务的执行结果。submit方法在ExecutorService中定义的,并定义了三种重载方式:

<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);Future<?> submit(Runnable task);

具体使用如下:

class MyCallable implements Callable<Integer>{  @Override  public Integer call() throws Exception {  return null;  }  
}// 提交callable任务,可以拿到返回值
Future<Integer> future1 = executorService.submit(new MyCallable());class MyRunnable implements Runnable {  @Override  public void run() {  }  
}
Future<Void> future2 = executorService.submit(new MyRunnable(), null);  
Future<?> future3 = executorService.submit(new MyRunnable());

主要特点:

  • 返回一个Future对象,可通过Futureget()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成。
  • 可以接受CallableRunnable类型的任务。
  • 执行RunnableCallable的任务时,run()/call()方法没显式抛出异常。
3.2.2 execute方法

execute方法用于提交实现了Runnable接口的任务,它没有返回值,因此无法获取任务的执行结果。如果任务执行过程中抛出了异常,线程池会捕获并记录该异常,但无法通过execute方法获知。execute方法是在线程池的顶级接口Executor中定义的,而且只有这一个接口。

public interface Executor {  void execute(Runnable command);  
}

使用:

executorService.execute(() -> {  // 具体业务逻辑  
});

主要特点:

  • 没有返回值,无法获取任务的执行结果。
  • 只能接受Runnable类型的任务。

总的来说,submit方法更加灵活,适用于更多场景,而execute方法更加简单,适用于只关心任务执行而不需要获取结果的场景。在实际应用中,根据具体需求选择合适的方法。如果需要获取任务的执行结果、取消任务等,建议使用submit方法。只是执行任务而不关心返回值,可以使用execute方法。

4. 线程池的参数以及配置

Java线程池的性能和行为可以通过一系列参数进行调整,以满足不同的并发需求。ThreadPoolExecutor中提供的构造器如下:

ThreadPoolExecutor.png

4.1 七大参数
4.1 核心线程数(Core Pool Size)

核心线程数是线程池中一直存活的线程数量(即使它们处于空闲状态)。这些线程用于执行提交到线程池的任务。通过合理设置核心线程数,可以在系统负载增加时迅速响应任务。

4.2 最大线程数(Maximum Pool Size)

最大线程数定义了线程池中允许创建的最大线程数量。当核心线程都在执行任务,而新任务仍然到来时,线程池会创建新线程,直到达到最大线程数。超过最大线程数的任务会被拒绝。

4.3 线程存活时间(Keep Alive Time)

线程存活时间指的是非核心线程在空闲状态下的最大存活时间。当线程池中线程数量超过核心线程数时,空闲的非核心线程在经过一定时间后会被终止,从而释放系统资源。

4.4 TimeUnit

keepAliveTime的单位(ms、s…)

4.5 工作队列(Work Queue)

工作队列用于存放等待执行的任务。不同类型的队列对线程池的行为有重要影响,例如有界队列和无界队列。有界队列在任务数达到上限时会触发拒绝策略。

4.6 ThreadFactory

线程池中生成线程的工厂。默认使用默认工厂Executors.defaultThreadFactory()。但是实际使用时建议使用Guava的ThreadFactory自定义线程的名字,方便排查线程问题(阿里开发规范中也建议这么做)。如下:

ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("mythread-pool-").build();
4.7 拒绝策略(Rejected Execution Policy)

拒绝策略定义了当工作队列满,并且当前工作的线程数等于最大线程数时,后续再提交的任务如何处理。例如,可以选择抛弃任务、抛出异常或在调用线程中直接执行。Java线程池提供了几种常见的拒绝策略:

  • AbortPolicy(默认策略):
    直接抛出RejectedExecutionException,阻止系统继续接受新任务,保持原有状态。
new ThreadPoolExecutor.AbortPolicy();
  • CallerRunsPolicy:
    将任务返回给调用者,由调用线程直接执行。
new ThreadPoolExecutor.CallerRunsPolicy();
  • DiscardPolicy:
    直接丢弃无法处理的任务,不抛出异常。
new ThreadPoolExecutor.DiscardPolicy();
  • DiscardOldestPolicy:
    当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。
new ThreadPoolExecutor.DiscardOldestPolicy();
4.2 线程池提交任务执行流程
4.2.1 执行流程

线程池执行流程.png

4.2.2 实例讲解

某银行柜台,共有5个窗口(Maximum Pool Size),平时常开2个窗口办理业务(Core Pool Size),银行大厅摆了5个椅子(Work Queue)供客户等待。银行规定当常开的窗口都在办理业务,并且大厅椅子上都坐满了客户,那么另外3个不常开的窗口也要打开办理业务。如果这3个窗口也都全部在办理业务,后面继续来银行办理业务的客户银行将拒绝办理。如果某个员工空闲下并且超过了5(Keep Alive Time)秒钟(TimeUnit)那么他就可以关闭窗口去休息。但是必须保留2个常开的窗口。
我们先按照上述流程创建一个线程池:

// 推荐使用Guava的ThreadFactory构建ThreadFactory,自定义线程名称 方便后续排查问题  
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("mythread-pool-").build();  
// 定义号线程  
ExecutorService executorService = new ThreadPoolExecutor(  // 核心线程数,即2个常开窗口  2,  // 最大的线程数,银行所有的窗口  5,  // 空闲时间  5,  TimeUnit.SECONDS,  // 工作队列  new LinkedBlockingQueue<>(5),  // 线程工厂  threadFactory,  // 拒绝策略  new ThreadPoolExecutor.AbortPolicy()  
);

(1)初始状态下,只有2个窗口为2个客户办理业务。

ThreadPool_演示1.png

(2)在客户1,客户2办理业务或者说常开窗口一直都有客户在办理业务,此时陆续有客户进来,需要在银行大厅的椅子上等待。

ThreadPool_图解2.png

(3)直到大厅的椅子都坐满。

ThreadPool_图解5.png

(4)此时如果在所有的窗口都在办理业务,大厅椅子坐满,如果再来一个客户,将开启3/4/5的窗口

ThreadPool_图解7.png

(5)此时如果在所有的窗口都在办理业务,大厅椅子坐满,还从外面再来2个客户办理业务,那么就需要把剩下的窗口都要打开去办理业务。

ThreadPool_图解8.png

(6)此时如果再来1个客户,就会按照线程池定义的拒绝策略去执行,比如我们设置策略为:AbortPolicy,就会抛出异常。

ThreadPool_图解9.png

4.3 线程池参数配置

线程池的配置参数在实际应用中需要根据具体的业务场景和性能需求进行巧妙调整。这就好比在日常生活中,如果有一个任务需要三人协同完成,但却有六人前来参与,就会造成三人的资源浪费;反之,若只安排两人协作,可能会超负荷而不切实际。因此,在线程池参数配置时,过小或过大都会带来问题。

当线程池数量设置过小时,面对大量同时到达的任务或请求,可能导致这些任务在任务队列中排队等待执行。甚至在任务队列达到最大容量时,无法处理额外的任务,或者导致任务队列积压,有可能引发内存溢出(OOM)问题。这明显是一个问题,因为CPU资源无法得到充分利用。

相反,若线程数量设置过大,大量线程可能会同时争夺CPU资源,导致频繁的上下文切换,从而增加线程的执行时间,影响整体执行效率。因此,在线程池配置中需要平衡线程数量,以满足高并发场景下的任务处理需求,同时避免不必要的资源争夺和上下文切换,以保障系统的稳定性和性能。

并没有一个通用的标准来设置参数,因此需要结合实际实战经验、业务需求以及服务器资源的状况,灵活而合理地进行参数配置。最终,合适的配置才是最为优越的选择。

当然也有一个简单而广泛适用的公式,可以用于确定线程池中的线程数:

  1. CPU 密集型任务(N+1):
    • 对于消耗主要是CPU资源的任务,可以将线程数设置为N(CPU核心数)+1。额外的一个线程用于防止线程偶发的缺页中断或其他原因导致的任务暂停,防止空闲时间的浪费。一旦任务暂停,多出来的一个线程可以充分利用CPU的空闲时间。
  2. I/O 密集型任务(2N):
    • 对于主要涉及I/O交互的任务,系统会在大部分时间内处理I/O,而在线程处理I/O的时间段内不会占用CPU。因此,在I/O密集型任务中,可以配置更多的线程,具体计算方法是2N。

那我们如何判断任务是CPU密集型还是IO密集型呢?简而言之,CPU密集型任务主要利用CPU计算能力,例如对内存中大量数据进行排序。而IO密集型任务涉及网络读取、文件读取等,其特点是CPU计算耗费的时间相对较少,大部分时间花在等待IO操作完成上。

但是我们在实际的业务中会发现,我们一个服务器上可能跑多种类型的业务,不太好判断到底是CPU密集任务还是IO密集型。我们可以根据监控服务线程池资源利用情况结合业务场景动态配制合理参数。这里我们就不得不提一下美团的线程池参数动态化配置:Java线程池实现原理及其在美团业务中的实践 - 美团技术团队。

5. 线程池的使用

日常开发中我们可以通过Executors去创建线程池,例如:
(1)newFixedThreadPool()

ExecutorService executorService1 = Executors.newFixedThreadPool(2);

创建固定线程数的线程池,核心线程数等于最大线程数,此时keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求,导致OOM。

(2)newSingleThreadExecutor()

ExecutorService executorService2 = Executors.newSingleThreadExecutor();

创建单线程的线程池,即核心线程数等于最大线程数均等于1,keepAliveTime失效 。但是他的工作队列的长度为Integer.MAX_VALUE。可能会导致堆积大量的请求,导致OOM。

(3)newCachedThreadPool()

ExecutorService executorService3 = Executors.newCachedThreadPool();

创建一个核心线程数等于0,并且允许创建的最大线程数等于Integer.MAX。keepAliveTime为60秒。可能会造成创建大量的线程,从而导致OOM。

(4)newScheduledThreadPool()

ExecutorService executorService4 = Executors.newScheduledThreadPool(2);

创建一个允许最大线程数等于Integer.MAX,但是他使用的阻塞工作队列是DelayedWorkQueueDelayedWorkQueue的核心数据结构是二叉最小堆的优先队列,队列满时会自动扩容。所以最大线程数没有意义,线程池中永远会保持至多有核心线程数个工作线程正在运行。

注意: 以上创建线程池的方法,可以做自己Demo使用,不应该用在项目中。在阿里巴巴代码规范中,不支持使用这种方式去创建,支持手动创建线程池。

ThreadPool_阿里巴巴开发规范.png

6.总结

Java线程池是多线程编程中的重要工具,能够有效管理和复用线程,提高系统性能和资源利用率。本文深入探讨了线程池的基础概念、工作原理、参数配置、自定义以及使用示例,并强调了注意事项。
通过了解线程池的工作原理,开发者可以更好地配置线程池以适应不同的并发需求。自定义线程池则使得线程池更灵活地适应特定业务场景。在实际应用中,要谨慎选择线程池类型、合理配置参数、注意任务的生命周期和线程安全等问题,以确保系统的稳定性和性能。

参考文献

1、Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)
2、《Java并发编程实战》

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python基础】一文搞懂:Python 中轻量型数据库 SQLite3 的用法

一文搞懂&#xff1a;Python 中轻量型数据库 SQLite3 的用法 文章目录 一文搞懂&#xff1a;Python 中轻量型数据库 SQLite3 的用法1 引言2 SQLite3 简介3 基本步骤4 示例代码4.1 连接数据库4.2 创建表4.3 插入数据4.4 查询数据4.5 更新/删除数据4.6 关闭数据库连接 5 实例演示…

NPN PNP磁性开关区别

自记&#xff1a; 网上有些前后内容是相反的&#xff0c;估计自己就没明白&#xff0c;此为分析后得出结论&#xff0c;看完后可懂 1、NPN&#xff08;源型&#xff09;&#xff1a;当导通时输出低电平 当导通时&#xff0c;信号输出线out和0v线连接&#xff0c;相当于输出低电…

OCP NVME SSD规范解读-6.标准日志要求-1

4.8 Log Page Requirements章节在NVMe规范中主要涵盖了设备应支持的日志页面&#xff08;Log Pages&#xff09;的要求。日志页面是存储控制器用于报告内部状态、性能统计和其他关键信息的结构化数据区域&#xff0c;它们对系统管理和故障诊断至关重要。 本文&#xff0c;我们…

行走在深度学习的幻觉中:问题缘由与解决方案

如何解决大模型的「幻觉」问题&#xff1f; 我们在使用深度学习大模型如LLM&#xff08;Large Language Models&#xff09;时&#xff0c;可能会遇到一种被称为“幻觉”的现象。没错&#xff0c;它并不是人脑中的错觉&#xff0c;而是模型对特定模式的过度依赖&#xff0c;这…

24/1/10 qt work

1. 完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配&…

特征工程:图像数据不足时的处理办法

在机器学习中&#xff0c;绝大部分模型都需要大量的数据进行训练和学习&#xff08;包括有监督学习和无监督学习&#xff09;&#xff0c;然而在实际应用中经常会遇到训练数据不足的问题。比如图像分类&#xff0c;作为计算机视觉最基本的任务之一&#xff0c;其目标是将每幅图…

红队打靶练习:TOMMY BOY: 1

目录 信息收集 1、arp 2、nmap 3、nikto 4、whatweb WEB robots.txt get flag1 get flag2 FTP登录 文件下载 更改代理 ffuf爆破 get flag3 crunch密码生成 wpscan 1、密码爆破 2、登录wordpress ssh登录 get flag4 信息收集 get flag5 信息收集 1、arp …

1-01初识C语言

一、概述 C语言是贝尔实验室的Ken Thompson&#xff08;肯汤普逊&#xff09;、Dennis Ritchie&#xff08;丹尼斯里奇&#xff09;等人开发的UNIX 操作系统的“副产品”&#xff0c;诞生于1970年代初。 Thompson和Ritchie共同创作完成了Unix操作系统&#xff0c;他们都被称为…

可编程线性霍尔传感器 IC

一、产品概述 CC6521/2 是一款高性能的可编程线性霍尔传感器 IC&#xff0c;采用先进的 BiCMOS 制程生产&#xff0c;具有霍尔系数高的优点&#xff0c;芯片内部包含了高灵敏度 霍尔传感器&#xff0c;霍尔信号预放大器&#xff0c;高精度的霍尔温度补偿单元&#xff0c;振荡…

【博士每天一篇文-算法】Graph Structure of Neural Networks

阅读时间&#xff1a;2023-11-12 1 介绍 年份&#xff1a;2020 作者&#xff1a;尤家轩 斯坦福大学 期刊&#xff1a; International Conference on Machine Learning. 引用量&#xff1a;130 论文探讨了神经网络的图结构与其预测性能之间的关系。作者提出了一种新的基于图的…

2024-01-01 K 次取反后最大化的数组和和加油站以及根据身高重建队列

1005. K 次取反后最大化的数组和 思路&#xff1a;每一次取反最小值即可&#xff01;贪心的思路就是先排序&#xff0c;反转负数的值&#xff0c;后在贪心反转最小值 class Solution:def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:count 0while …

Unity中URP下开启和使用深度图

文章目录 前言一、在Unity中打开URP下的深度图二、在Shader中开启深度图1、使用不透明渲染队列才可以使用深度图2、半透明渲染队列深度图就会关闭 三、URP深度图 和 BRP深度图的区别四、在Shader中&#xff0c;使用深度图1、定义纹理和采样器2、在片元着色器对深度图采样并且输…

LeetCode刷题--- 最小路径和

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏 力扣递归算法题 http://t.csdnimg.cn/yUl2I 【C】 ​​​​​​http://t.csdnimg.cn/6AbpV 数据结构与算法 ​​​http://t.csdnimg.cn/hKh2l 前言&#xff1a;这个专栏主要讲述动…

setup 语法糖

只有vue3.2以上版本可以使用 优点&#xff1a; 更少的样板内容&#xff0c;更简洁的代码 能够使用纯 Typescript 声明props 和抛出事件 更好的运行时性能 更好的IDE类型推断性能 在sciprt标识上加上setup 顶层绑定都可以使用 不需要return &#xff0c;可以直接使用 使用组件…

Redis异步写失败后补数逻辑设计

背景 最近各种机房事故频发&#xff0c;所以很多公司都对Redis存储等进行异步多活&#xff0c;我们公司采用的方式是通过客户端双写的方式来实现异地Redis机房的备份&#xff0c;但是当异地机房出现临时网络故障时&#xff0c;就涉及到了如何进行补数的操作&#xff0c;本文就…

理解Herbrand Equivalence

笔者最近在看GVN的一系列论文&#xff0c;总会看到一个概念叫Herbran Equivalence&#xff0c;依靠这种定义&#xff0c;能够判断一个GVN算法是否是complete的&#xff0c;也即检测一个算法是否是precise的&#xff0c;只有找到所有Herbrand Equivalence关系的算法才能称得上是…

2024.1.10

完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配&#xf…

01-你好Python-python环境安装 python解释器的安装 pycharm的安装

python环境安装 官方网址&#xff1a;https://python.org 这里可以下载最新版本的&#xff0c;下载完成以后在自己的浏览器文件下载的文件夹中找到该文件 下载速度可能会比较慢&#xff0c;这里已经提供好了文件&#xff0c;可以直接点击安装 点击Customize installation 点击…

pulsar的架构与特性记录

一、什么是云原生 云原生的概念是2013年Matt Stine提出的,到目前为止&#xff0c;云原生的概念发生了多次变更&#xff0c;目前最新对云原生定义为: Devps持续交付微服务容器 而符合云原生架构的应用程序是: 采用开源堆栈(K8SDocker)进行容器化&#xff0c;基于微服务架构提高灵…

人工智能利用深度学习技术增强高级驾驶辅助系统(ADAS)

深度学习通过实时传感器数据增强高级驾驶辅助系统(ADAS)&#xff0c;实现精确的物体检测、碰撞预测和主动决策。 人工智能和机器学习利用深度学习技术的优势&#xff0c;使高级驾驶辅助系统(ADAS)发生了重大变革。ADAS在很大程度上依赖深度学习来分析和解释从各种传感器获得的…