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.mfbz.cn/a/308006.html

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

相关文章

PHP Web应用程序中常见漏洞

一淘模板&#xff08;56admin.com)发现PHP 是一种流行的服务器端脚本语言&#xff0c;用于开发动态 Web 应用程序。但是&#xff0c;与任何其他软件一样&#xff0c;PHP Web 应用程序也可能遭受安全攻击。 在本文中&#xff0c;我们将讨论 PHP Web 应用程序中一些最常见的漏洞…

linux异常情况,排查处理中

登录客户环境后&#xff0c;发现一个奇怪情况如下图&#xff0c;之前也遇到过&#xff0c;直接fuser -ck /backup操作的话&#xff0c;主机将会重启&#xff0c;因数据库运行中&#xff0c;等待停机维护时间&#xff0c;同时也在想办法不重启的情况下解决该问题 [rootdb ~]# f…

使用西瓜视频官网来创造一个上一集,下一集的按钮,进行视频的切换操作

需求: 仿照西瓜视频写一个视频播放和上一集下一集的按钮功能 回答: 先访问官网: 西瓜播放器 这是西瓜视频的官网, 点击官网的示例按钮,可以看到相关的视频示例以及相关的代码, 我们复制下来代码,然后添加按钮和切换视频的方法, 完整代码: <!DOCTYPE html> <ht…

Hotspot源码解析-第十七章-虚拟机万物创建(三)

17.4 Java堆空间内存分配 分配Java堆内存前&#xff0c;我们先通过两图来了解下C堆、Java堆、内核空间、native本地空间的关系。 1、从图17-1来看&#xff0c;Java堆的分配其实就是从Java进程运行时堆中选中一块内存区域来映射 2、从图17-2&#xff0c;可以看中各内存空间的…

Springboot3(一、lambda、::的应用)

文章目录 一、使用lambda简化实例创建1.语法&#xff1a;2.示例&#xff1a;3.Function包3.1 有入参&#xff0c;有返回值【多功能函数】3.2 有入参&#xff0c;无返回值【消费者】3.3 无入参&#xff0c;有返回值【提供者】3.4 无入参&#xff0c;无返回值 二、类::方法的使用…

基于ssm运动会管理系统的设计与实现 【附源码】

基于ssm运动会管理系统的设计与实现 【附源码】 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuil…

C2-3.3.2 机器学习/深度学习——数据增强

C2-3.3.2 数据增强 参考链接 1、为什么要使用数据增强&#xff1f; ※总结最经典的一句话&#xff1a;希望模型学习的更稳健 当数据量不足时候&#xff1a; 人工智能三要素之一为数据&#xff0c;但获取大量数据成本高&#xff0c;但数据又是提高模型精度和泛化效果的重要因…

工业智能网关:HiWoo Box远程采集设备数据

工业智能网关&#xff1a;HiWoo Box远程采集设备数据 在工业4.0和智能制造的浪潮下&#xff0c;工业互联网已成为推动产业升级、提升生产效率的关键。而在这其中&#xff0c;工业智能网关扮演着至关重要的角色。今天&#xff0c;我们就来深入探讨一下工业智能网关。 一、什么…

Apache JMeter 5.5: 新手指南

如何获取并运行 JMeter 首先&#xff0c;要使用 JMeter&#xff0c;你需要从官网获取软件包。前往 Apache JMeter 的官方页面&#xff0c;然后下载所 需的压缩文件。 配置和启动 JMeter 获取了 JMeter 后&#xff0c;由于它是无需安装即可使用的工具&#xff0c;直接解压下载…

申请企业通配符SSL证书流程

通配符SSL证书&#xff0c;又叫泛域名SSL证书&#xff0c;可以用一张SSL证书同时保护主域名以及主域名下的所有子域名。按照验证方式可以将通配符SSL证书分为DV通配符SSL证书和OV通配符SSL证书。其中OV通配符SSL证书只支持企事业单位申请&#xff0c;又称之为OV企业型通配符SSL…

贝蒂详解<string.h>(下)

✨✨欢迎大家来到贝蒂大讲堂✨✨ ​​​​&#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C语言学习 贝蒂的主页&#xff1a;Betty‘s blog 目录 1. 简介 2. memset()函数 2.1用法 2.2实例 2.3 实现me…

快速了解云计算与云原生

快速了解云计算与云原生 云计算云原生DevOps容器持续交付微服务 云计算 在讲云原生之前&#xff0c;先来讲讲云计算 其中云原生属于技术架构理念&#xff0c;而云计算提供应用所需的基础资源&#xff0c;云计算是云原生的基础&#xff0c;两者是相辅相成的 云计算简单来说&a…

竞赛保研 基于深度学习的水果识别 设计 开题 技术

1 前言 Hi&#xff0c;大家好&#xff0c;这里是丹成学长&#xff0c;今天做一个 基于深度学习的水果识别demo 这是一个较为新颖的竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f9ff; 更多资料, 项目分享&#xff1a; https://gitee.com/dancheng-senior/pos…

JVM基础(3)——JVM垃圾回收机制

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…

复合机器人作为一种新型的智能制造装备高效、精准和灵活的生产方式

随着汽车制造业的快速发展&#xff0c;对于高效、精准和灵活的生产方式需求日益增强。复合机器人作为一种新型的智能制造装备&#xff0c;以其独特的优势在汽车制造中发挥着越来越重要的作用。因此&#xff0c;富唯智能顺应时代的发展趋势&#xff0c;研发出了ICR系列的复合机器…

计算机毕业设计 | SpringBoot航空订票 机票预定购买系统(附源码)

1&#xff0c; 概述 1.1 选题目的 目前&#xff0c;国内航空公司的数量和规模都在扩大&#xff0c;国外航空公司也纷纷着陆中国&#xff0c;这些航空公司之间的竞争可谓日益激烈。配备一个安全、高效、灵活、可靠的客户服务中心系统对于航空公司加强客户服务质量&#xff0c;…

使用Android Compose实现网格列表滑到底部的提示信息展示

文章目录 概述1 效果对比1.1 使用添加Item的办法&#xff1a;1.2 使用自定义的方法 2. 效果实现2.1 列表为空时的提示页面实现2.2 添加Item的方式代码实现2.3 使用自定义的方式实现 3. UI工具类 概述 目前大多数的APP都会使用列表的方式来呈现内容&#xff0c;例如淘宝&#x…

解决Echarts y轴文本超出容器问题

解决Echarts y轴文本超出容器问题 一开始好好的 数据变多之后就被挤出去了 解决方法&#xff1a; // echarts的grid属性 主要就是containLabel这个属性的配置 不设置的话他默认是false, 主要是包含是否包含刻度标签grid: {left: "5%",right: "10%",botto…

linux 里面在docker 里面安装pg 数据库(亲测有效)

目录 1 上传 1 上传 上传之后tar 包&#xff0c;将他变成镜像 输入docker images,发现目前是没有镜像的&#xff0c;现在将tar 包变成镜像 docker load -i postgresql.tar以上就将tar 包变成镜像了 现在在宿主机找一个地方&#xff0c;存放数据库的数据 /home/softinstall/…

全网独家:基于openEuler-20.03-LTS-SP4底包构建opengaussV5.0.1LTS的单机极简版数据库容器

本文尝试基于openEuler-20.03-LTS-SP4底包构建opengaussV5.0.1LTS的单机版极简版数据库容器。 一、软件包源 1、openEuler-20.03-LTS容器底包 openEuler-20.03-LTS-SP4 下载链接 sha256:24d8f51c1f3a79eb975c4e498cadd9055bfd708d66c15935ec46664d0f975a7b openEuler-dock…
最新文章