Java 线程池详解

news/2024/11/24 16:36:29/

一、概念

线程池(thread pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中。线程过多 会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,对线程统一管理。

二、使用线程池的优势

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

三、线程池解决的问题

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。

  1. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。

  1. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:

  1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。

  1. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。

  1. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

四、线程池的结构

Java中的线程池核心实现类是ThreadPoolExecutor。ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制如下图所示:

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

接下来,我们会按照以下三个部分去详细讲解线程池运行机制:

  1. 线程池如何维护自身状态。

  1. 线程池如何管理任务。

  1. 线程池如何管理线程。

4.1 Executor

它是 Java 异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者”Executor提供了 execute()接口来执行已提交的 Runnable 执行目标实例。Executor 作为执行者的角色,存在的目的是“任务提交者”与“任务执行者”分离开来的机制

4.2 ExecutorService

ExecutorService 继承于 Executor。。它是 Java 异步目标任务的“执行者服务“接口,它对外提供异步任务的接收服务,ExecutorService 提供了“接收异步任务、并转交给执行者”的方法,如submit 系列方法、invoke 系列方法等等。

4.3 AbstractExecutorService

AbstractExecutorService 是一个抽象类 ,它 实 现 了 ExecutorService 接口。AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供了默认实现。

4.5 ScheduledExecutorService

ScheduledExecutorService 是一个接口,它继承于于 ExecutorService。它是一个可以完成“延 时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似。

4.6 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,它提供了 ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。ScheduledThreadPoolExecutor 类似于 Timer , 但 是 在 高 并 发 程 序 中ScheduledThreadPoolExecutor 的性能要优于 Timer

五、Executors 四种快捷创建线程池方法

Executors 是个静态工厂类,它通 过 静 态 工 厂 方 法 返 回 ExecutorService 、ScheduledExecutorService 等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法

1、Executors 四种快捷创建线程池方法

newCacheTreadPool

创建一个可以缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程

 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();for (int i = 0; i < 5; i++) {//创建任务Runnable runnable = new Runnable(){@Overridepublic void run() {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}};newCachedThreadPool.execute(runnable);}

newFixedThread

创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。

 ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);for (int i = 0; i < 5; i++) {//创建任务Runnable runnable = new Runnable(){@Overridepublic void run() {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}};// 将任务交给线程池管理newFixedThreadPool.execute(runnable);}

newScheduleThreadPool

可以创建定长的、支持定时任务,周期任务执行。

  ScheduledExecutorService  newScheduledThreadPool = Executors.newScheduledThreadPool(2);for (int i = 0; i < 5; i++) {//创建任务Runnable runnable = new Runnable(){@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};// 将任务交给线程池管理,延迟2秒后才开始执行线程池中的所有任务newScheduledThreadPool.schedule(runnable, 2, TimeUnit.SECONDS);}

newSingleExecutor

创建一个单线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ExecutorService   newScheduledThreadPool = Executors.newSingleThreadExecutor();for (int i = 0; i < 5; i++) {//创建任务Runnable runnable = new Runnable(){@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};// 将任务交给线程池管理newScheduledThreadPool.execute(runnable);}

ThreadPoolExecutor 类介绍

ThreadPoolExecutor 是线程池最为核心的一个类,而线程池为它提供了四个构造方法,我们先来看一下其中最原始的一个构造方法,其余三个都是由它衍生而来

/*** 用给定的初始参数创建一个新的ThreadPoolExecutor。*/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.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}

可以看到这里有6个参数,这些参数直接影响到线程池的效果,以下是具体分析每个参数的意义

1、corePoolSize:线程池最小创建线程的数目,默认情况下,线程池中是没有线程的,也就是当没有任务来临的时候,初始化的线程池容量为0,而最小创建线程的数目则是在有线程来临的时候,直接创建 corePoolSize 个线程

2、maximumPoolSize:线程池能创建的最大线程的数量,在核心线程都被占用的时候,继续申请的任务会被搁置在等待队列里面,而当等待队列满了的时候,线程池就会把线程数量创建至 maximumPoolSize 个。

3、workQueue:核心线程被占有时,任务被搁置在任务队列

4、keepAliveTime:当线程池中的线程数量大于 corePoolSize 时这个参数就会生效,即当大于 corePoolSize 的线程在经过 keepAliveTime 仍然没有任务执行,则销毁线程

5、unit :参数keepAliveTime的时间单位

6、ThreadFactory:线程工厂:主要用来创建线程,一般默认即可

7、handler:饱和策略,即当线程池和等待队列都达到最大负荷量时,下一个任务来临时采取的策略

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

ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。

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

ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。

ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

ThreadPoolExecutor的状态有5种,分别为:

  • RUNNING,表示可接受新任务,且可执行队列中的任务;

  • SHUTDOWN,表示不接受新任务,但可执行队列中的任务;

  • STOP,表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务;

  • TIDYING,所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法;

  • TERMINATED,中止状态,已经执行完terminated()钩子方法;

3、线程池处理任务的策略

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;

  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

4、 workQueue任务队列

workQueue任务队列一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列;

1、直接提交队列synchronousQueue: 这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务

2、有界任务队列ArrayBlockingQueue: 基于数组的先进先出队列,此队列创建时必须指定大小;

3、无界任务队列LinkedBlockingQueue: 基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

4、优先任务队列PriorityBlockingQueue: 优先任务队列通过PriorityBlockingQueue实现

5、为什么不推荐使用 Executors 去创建线程池?

5.1 内存占用

从上面可知, Executors可以大致可以通过四种方式进行创建线程池。FixedThreadPool、SingleThreadPool都有固定数量线程的线程池,但核心线程数与最大线程数相等。任务队列的大小没有设置,是默认的,也就是无界的。所以有可能会导致其无限增大,最终内存撑爆。CachedThreadPool核心线程数是0,拥有最大的执行线程数,但是默认的任务队列中只能存一个,可以认为所有放到 newCachedThreadPool() 中的线程,不会缓存到队列中,而是直接运行的。随着执行线程数量的增多和线程没有及时结束,最终会将内存撑爆。ScheduledThreadPool可以创建固定数量的核心线程,可以延迟或定时的执行任务。但最大执行线程数也是无限的,同样会导致内存撑满。

5.2 拒绝策略不能自定义

Executors 底层也是通过 ThreadPoolExecutor 创建的,但 ThreadPoolExecutor 的默认策略,即 AbortPolicy。当线程无法执行新任务时,一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的,会直接抛出异常。


http://www.ppmy.cn/news/22856.html

相关文章

3.Golang面试题—Gin框架、sqlx

本文目录如下&#xff1a;九、Gin框架简单介绍一下 Gin 框架 (为什么选择Gin框架)&#xff1f;Gin框架的开发步骤&#xff1f;YAML 配置的优势在哪里 ?什么是 viper&#xff1f;什么是JWT&#xff1f;Gin框架中如何使用Token&#xff1f;简单介绍下 Gin语言 中的中间件&#x…

纯css实现3d立方体旋转相册

前言 现如今网页越来越趋近于动画&#xff0c;相信大家平时浏览网页或多或少都能看到一些动画效果&#xff0c;今天我们来做一个有意思的动画效果&#xff0c;通过 css3 实现 3d 效果的立方体相册&#xff0c;下面一起看看吧。 实现思路 首先我们要确定好 html 的结构以及要使…

海量电商商品图片模板,在线就能轻松设计

节日礼物类的商品要如何设计主图&#xff1f;如何设计和美化礼品主图才能让顾客产生购买的想法?今天跟着小编的设计教程&#xff0c;教你如何使用在线工具乔拓云&#xff0c;轻松设计电商商品的主图&#xff01;不用自己动手排版轻松出图&#xff0c;跟着小编下面的图文教学一…

. netCore NLog 自定义Render总结

NLog 自定义Render总结 Nlog在格式化输出日志过程中&#xff0c;如果变量不够用&#xff0c;则需要自定义变量实现。具体自定义变量也很简单&#xff0c;只需添加自定义LayOutRenderer即可。具体实现步骤如下&#xff1a; 第一步&#xff1a;继承抽象类LayoutRenderer [Layo…

【算法基础】高精度乘法

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前是C语言 算法学习者 ✈️专栏&#xff1a;【C/C】算法 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac…

python爬虫--requests简介

一&#xff1a;requests的概念 简单来说&#xff0c;爬虫由获取网页和解析网页获取数据组成&#xff0c;reqiuests模块就是用来获取网页的&#xff0c;当然requests模块时第三方模块&#xff0c;需要下载导入&#xff08;winr--->pip install requests&#xff09;,另外 使…

Ubuntu 22.04更新源报错404 Not Found

拉取软件源报错&#xff1a; E: Failed to fetch http://mirrors.aliyun.com/ubuntu/dists/jammy/main/binary-arm64/Packages 404 Not Found [IP: 140.249.32.239 80] E: Failed to fetch http://mirrors.aliyun.com/ubuntu/dists/jammy-security/main/binary-arm64/Packages…

Vue极简使用

Vue安装Vue模板语法安装Vue 安装nodejs 这里我安装的是14.5.4版本 https://nodejs.org/download/release/v14.15.4/解压后配置一下环境变量就行 安装cnpm镜像 (这个安装的版本可能过高&#xff0c;后面安装Vue可能出问题) npm install -g cnpm --registryhttps://registry…