java线程池最大线程数-java 线程池框架
什么是线程池?
线程池用于多线程处理。 可以根据系统情况有效控制线程执行数量,优化运行效果。 线程池的工作主要是控制运行线程的数量。 在处理过程中,将任务放入队列中,然后创建线程后启动这些任务。 如果线程数超过最大数,则超出的线程排队等待其他线程执行。 完成后,将任务从队列中取出执行。
线程池的作用
在面向对象的编程中,创建和销毁对象是非常耗费时间和资源的。 因此,最小化这种消耗的一个想法是“汇集资源”。 线程池就是这样一个思路。 我们通过重用线程池中的资源来减少创建和销毁线程所需的时间和资源。
线程池的一个作用是创建和销毁线程的数量,每个工作线程可以多次使用; 另一个功能是根据系统情况调整执行的线程数,防止过多的内存消耗。 另外,线程池可以有效控制线程的最大并发数,提高系统资源的利用率,避免资源过度竞争和阻塞。
线程池的优点总结起来有以下几个方面:
线程池的组成
通用的线程池主要分为以下四个部分:
线程池管理器:用于创建和管理线程池
工作线程:线程池中的线程
任务接口:每个任务必须实现的接口,工作线程使用它来调度它的运行
任务队列:用于存放待处理的任务,提供缓冲机制
线程池的常见应用场景
许多服务器应用程序经常需要处理大量的短请求(例如,Web 服务器、数据库服务器等),通常它们会收到大量的请求。 一个简单的模型是,当服务器收到来自远程的请求时,每个请求启动一个线程,请求完成后线程销毁。 这种方法的问题在于,创建和销毁线程所花费的时间往往远大于任务本身所消耗的资源。 所以我该怎么做?
线程池为线程生命周期开销问题和资源匮乏问题提供了解决方案。 我们可以通过线程池实现线程复用,不需要频繁创建和销毁线程,让线程池中的线程一直存在于线程池中,然后线程从任务队列中获取任务执行。 而这样做的另一个好处是,通过适当调整线程池中的线程数,即当请求数超过一定阈值时,其他任何新到达的请求都将被强制等待,直到获得线程进行处理。 ,从而防止资源匮乏。
Java线程池介绍
Java提供了实现线程池的框架Executor,并提供了多种类型的线程池,下一篇文章会详细介绍。
Java线程池框架
Java中的线程池是通过Executor框架实现的,Executor框架使用了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask。
Java线程池
ThreadPoolExecutor的构造方法如下:
ThreadPoolExecutor 的构造函数
在:
Java线程池的工作过程
Java线程池的工作过程如下:
线程池刚创建的时候,里面是没有线程的。 任务队列作为参数传入。 但是,即使队列中有任务,线程池也不会立即执行。
当调用execute()方法添加任务时,线程池会做如下判断:
当一个线程完成一个任务时,它会从队列中取出下一个任务来执行。
当线程无事,超过一定时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,则停止该线程。 所以线程池的所有任务完成后,最终会收缩到corePoolSize的大小。
普通Java线程池
Executors 的工厂方法用于生成线程池。 以下是常见的Java线程池:
单线程执行器
SingleThreadExecutor是单线程的线程池,即线程池中一次只有一个线程运行,单线程串行执行任务。 如果唯一的线程异常结束,一个新的线程将取代它。 这个线程池保证所有任务的执行顺序按照任务提交的顺序执行。
单线程执行器固定线程池
FixedThreadPool 是固定数量的线程池。 只有核心线程。 每提交一个任务,就是一个线程,直到达到线程池的最大数量,然后进入等待队列,继续执行,直到前面的任务完成。 一旦线程池的大小达到最大值,它将保持不变。 如果一个线程因为异常执行而结束,线程池会补充一个新的线程。
固定线程池缓存线程池
CachedThreadPool 是一个可缓存的线程池。 如果线程池的大小超过了处理任务所需的线程数,一些空闲线程(60秒内没有执行任务)将被回收。 当任务数量增加时,线程池可以智能地添加新的线程来处理任务。 这个线程池不限制线程池的大小,线程池的大小完全取决于操作系统(或JVM)可以创建的最大线程大小。 其中,SynchronousQueue是一个buffer为1的阻塞队列。
缓存线程池调度线程池
ScheduledThreadPool是一个线程池,核心线程池固定,大小不限,支持定时和周期执行线程。 创建一个周期性执行任务的线程池。 如果空闲,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内被回收。
调度线程池
我们可以通过Executors的工厂方法来创建线程池。 但是我们如何让线程池执行任务呢?
线程池最常用的提交任务的方法有两种:
执行
提交
可以看出,submit启动了一个返回结果的task,返回的是一个FutureTask对象,这样就可以通过get()方法获取到result了。 提交最后调用execute(Runnable runable)。 submit 只是将 Callable 对象或 Runnable 封装成一个 FutureTask 对象。 因为FutureTask是一个Runnable,所以可以在execute中执行。
以下示例代码演示了如何创建线程池并使用它来管理线程:
运行结果:
Java线程池原理
本文将从这三个方面,结合具体的代码实现,分析Java线程池的原理及其具体实现。
线程重用
我们知道线程池的一个作用就是线程的创建和销毁次数,每个工作线程可以多次使用。 这个功能就是线程复用。 要了解Java线程池是如何复用线程的,我们首先需要了解线程的生命周期。
线程生命周期
下图描述了一个线程完整的生命周期:
一个线程的完整生命周期
在一个线程完整的生命周期中,可能会经历五个状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、终止(Zombie)。
在Java中,Thread通过new创建一个新的线程。 这个过程就是初始化一些线程信息,比如线程名,id,线程所属组等,可以认为只是一个普通的对象。 调用Thread的start()后,Java虚拟机为其创建方法调用栈和程序计数器,同时将hasBeenStarted设置为true,之后如果再次调用start()方法,就会出现异常。
处于这种状态的线程并没有开始运行,它只是意味着线程已经准备好运行了。 至于线程什么时候开始运行,取决于JVM中线程调度器的调度。 当线程获得CPU时,会调用run()方法。 不要自己调用 Thread 的 run() 方法。 然后根据CPU调度,线程会在就绪-运行-阻塞之间切换,直到run()方法结束或者线程被其他方式停止,进入终止状态。
因此,如果我们要实现线程复用,就必须保证线程池中的线程保持存活状态(就绪、运行、阻塞)。 接下来我们看看ThreadPoolExecutor是如何实现线程复用的。
工人阶级
ThreadPoolExecutor主要通过一个类:Worker类来控制线程复用。
我们来看看简化后的Worker类代码:
从代码中我们可以看出,Worker实现了Runnable接口,它还有一个Thread成员变量thread,也就是开始运行的线程。 我们看到Worker的构造方法中传入了一个Runnable参数,它把自己作为参数传递给了newThread()。 这样,当调用Thread的start()方法时,真正执行的是Worker的run()方法。 ,即 runWorker() 方法。
runWorker()方法中有一个while循环java线程池最大线程数,使用getTask()获取任务并执行。 接下来,我们将看到 getTask() 如何获取 Runnable 对象。
获取任务()
让我们看一下简化的 getTask() 代码:
我们可以看到任务是从workQueue中获取的。 这个workQueue就是我们初始化ThreadPoolExecutor时存放任务的BlockingQueue队列。 所有要执行的 Runnable 任务都存储在这个队列中。 因为BlockingQueue是一个阻塞队列,如果BlockingQueue.take()返回空,则进入等待状态,直到BlockingQueue加入新的对象唤醒被阻塞的线程。 所以一般情况下,Thread的run()方法不会结束,而是会继续执行workQueue中的Runnable任务,达到了线程复用的目的。
控制最大并发数
我们现在知道了Java线程池是如何实现线程复用的,但是Runnable什么时候放入workQueue队列,Worker中的Thread什么时候调用start()启动一个新的线程执行Worker的run()方法呢? 从上面的分析我们可以看出,Worker中的runWorker()是一个一个的、串行的执行任务,那么并发性是如何体现的呢? 它是如何控制最大并发数的?
执行()
上面的一些问题看execute()就可以回答了,同样是一段简化的代码:
添加工人()
我们来看一下addWorker()的简化代码:
根据上面的代码,就很清楚线程池工作时如何添加任务了:
如果通过addWorker()成功创建了一个新线程,则通过start()启动一个新线程,同时在这个Worker中使用firstTask作为run()中第一个执行的任务。 虽然每个Worker的任务是串行处理的,但是如果创建了多个Worker,它们会被并行处理,因为它们共享一个workQueue。 所以可以根据corePoolSize和maximumPoolSize来控制最大并发数。
流程如下图所示:
管理线程
上面文章提到,线程池可以用来管理线程复用,控制并发数,销毁进程,线程管理过程穿插在其中,也很好理解。
ThreadPoolExecutor中有一个AtomicInteger变量ctl,里面有两个内容:
其中,低29位存储线程数,高3位存储runState,通过位运算得到不同的值。
这里主要通过shutdown和shutdownNow()来分析线程池的关闭过程。 首先,线程池有五个状态来控制任务的添加和执行。 主要介绍以下三种:
shutdown()方法会将runState设置为SHUTDOWN,并会终止所有空闲线程,而仍在工作的线程不会受到影响,因此队列中的任务会被执行; shutdownNow() 方法会将 runState 设置为 STOP。 与 shutdown() 方法的区别在于,该方法会终止所有线程,因此不会执行队列中的任务。
Java线程池框架源码分析
Java线程池框架中几个重要类的关系图在上一篇文章中已经给出:
下面我们就根据这张图一步一步来分析。
执行者
该接口表示向线程池提交任务。
执行服务
可以看到ExecutorService扩展了Executor接口,在Executor的基础上提供了更多提交任务和管理线程池的方式。
抽象执行服务
线程池执行器
ThreadPoolExecutor 是线程池框架的关键类。 我们先来看ThreadPoolExecutor中的几个重要属性。
workerCount和runState用一个AtomicInteger封装,runState使用int的高3位,低位代表workerCount,所以我们可以看到ThreadPoolExecutor中ctl相关的常量和解析方法。
ThreadPoolExecutor 的主要构造函数设置了上面提到的重要属性。
现在看execute()方法,execute()方法有3个处理步骤:
当线程数小于corePoolSize时,尝试创建新的工作线程
如果上述步骤失败,尝试将任务添加到阻塞队列中,再次判断是否需要回滚队列,或者创建线程
如果以上两步都失败,它会尝试强行创建一个线程来执行任务java线程池最大线程数,如果还是失败,则丢弃任务
解释第二步,为什么复查
当这个任务加入阻塞队列时,池是RUNNING状态,但是如果池成功加入队列后进入SHUTDOWN状态或者其他状态,此时不应该接收新的任务,所以这个任务需要从队列中移除,并拒绝
同样,在加入队列之前可能有一个有效的线程,但是在加入任务之后,线程空闲超时或者异常被杀死。 这时候需要创建一个新的线程来执行任务
为了更直观的了解一个任务的执行过程,可以参考下图
添加工人()
上一步我经历了execute的过程,里面多次出现了addWorker()方法。 前面说了,这是创建线程的方法。 我们来看看 addWorker 做了什么。 这个方法的代码比较长,我们拆开来看。 点击查看。
创建工人
上面的代码从逻辑上来说不难理解。 一个task到达这里后,ThreadPoolExecutor的处理就结束了,那么这个task是如何加入到阻塞队列中的,线程是如何从队列中移除task的。 以上什么是Worker?
一一来,我们来看看Worker是什么。
工人
Worker是ThreadPoolExecutor的内部类,实现了Runnable接口,继承自AbstractQueuedSynchronizer。 这到底是什么? ? ? 这是经常看到的AQS的全称,这个还没研究呢~~~~
简单的说,Worker实现了lock和unLock方法来表示当前线程状态是否空闲
上一节创建线程成功后调用t.start(),这个线程是Worker的成员变量
可以看到这里新建了一个线程,Worker 为 Runnable 参数。 我们知道,Thread接收到一个Runnable对象后,就开始运行Runnable的run方法。 Worker的run方法调用runWorker,就是取出任务执行的逻辑。
这里有一点很清楚。 当进入循环并准备执行任务时,工作人员用锁将其标记为非空闲。 任务执行完毕或出现异常后,worker释放锁,进入空闲状态。
也就是说,当一个worker执行一个任务或者执行完一个任务的时候,它是空闲的,在取出下一个任务的时间里可以被打断。
上面的task调用了getTask(),咦~怎么死循环了,别着急,慢慢看。上面的代码可以知道getTask如果返回一个task,就会执行,如果返回null,则工人需要回收
getTask()方法的逻辑差不多完成了,这里新增了两个方法workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)和workQueue.take(),都是阻塞队列方法。 让我们看看让我们看看他们是怎么做到的
LinkedBlockingQueue — 阻塞队列
ThreadPoolExecutor使用了一个链表结构的阻塞队列,实现了BlockingQueue接口,而BlockingQueue继承自Queue接口,上层是Collection接口。
因为这篇笔记主要是分析ThreadPoolExecutor的原理,所以不再详细介绍LinkedBlockingQueue中的其他代码。 主要介绍这里用到的方法。 首先我们来看一下上面提到的take()。
上面代码可以知道take方法会阻塞,直到队列中有新的任务
接下来是轮询方法。 可以看出和take方法差不多。 唯一不同的是在阻塞循环代码块中增加了一个时间判断。 如果超时,直接返回空,不会一直阻塞。
线程池的回收与终止
上一节分析了task的执行流程和原理,也留下了一个问题,worker是怎么回收的? 线程池应该怎么管理?回到上一节的runWorker()方法,记得最后调用了一个方法
该方法传入两个参数,第一个是当前Woker,第二个是标记异常退出的flag
先判断是不是异常退出。 如果是异常退出,需要手动调整线程数。 如果正常回收,说明你在getTask方法中手动调整过了。 如果你不记得,你可以看看前面的代码,寻找 decrementWorkerCount() ,
上面的代码调用了tryTerminate()方法,这个方法是用来终止线程池的,是一个for循环,从代码结构上看是一种异常情况的重试机制。还是老办法,总的来看一下完成的事情的数量
尝试终止线程池的代码分析完了,貌似结束了~不过作为好奇宝宝,是不是应该看看如何中断空闲线程,terminated中做了什么?来吧,继续装
先看中断线程
有同学开始装逼,说我们是好奇宝宝,t.interrupt()方法也要看,嗯~是的,不过这里是native方法,懂c的可以去看看在它,我会忘记它~
好吧,我们再看看terminate,是不是作弊了? 终结神在里面! 马! 还! 没有! 干燥! . . . 冷静点,其实这个方法类似于Activity的生命周期方法,让你在终止的时候做一些事情。 默认的线程池什么的,当然什么都不写了~
异常处理
还记得前面说的,出现各种异常情况,添加队列失败等等,我只是笼统的说扔掉。 当然代码实现也不能简单的扔掉。回到execute()方法,找到reject()任务,看看是怎么处理的
请记住,在创建线程池时,会初始化一个处理程序——RejectedExecutionHandler
这是一个只有一个方法接收两个参数的接口
既然是接口,就一定有它的实现类。 我们不要急于查看所有的实现类。 让我们看看这里的处理程序可能是什么。 记住在使用Executors获取线程池调用构造函数时,没有传入.handler参数,那么ThreadPoolExecutor应该有一个默认的handler
默认的handler是AbortPolicy,它实现了rejectedExecution()方法并抛出Runtime异常,也就是说当任务添加失败时,会抛出异常。 这个类在AsyncTask引起了血腥~所以在API19之后修改了AsyncTask的部分代码逻辑,这里不再赘述。
其实ThreadPoolExecutor中除了AbortPolicy之外,还实现了三种不同类型的handler