为什么使用线程池
在生产环境中,我们经常面临这样的情况:一个请求的处理时间很短,但是请求的数量很大。
在这种情况下,如果为每个请求分别创建一个线程,那么OS可以使用有限的硬件资源来创建线程。这些操作,如切换线程状态和销毁线程,将消耗更少的资源进行业务处理。
因此,理想的处理方法是将请求中的线程数控制在一个范围内,不仅可以保证后续请求不会等待太长时间,还可以保证物理机为请求处理本身使用足够的资源。
线程池的设计与结构
开发人员通常使用ThreadPoolExecutor类的构造函数创建具有不同配置的线程池:
通过这种施工方法,可以大致勾勒出水池的基本组成,其大致结构如下图所示。
注意:线程对象必须存在于线程池中,而不是要处理的任务中。因此,它被称为线程池,而不是任务池。线程池在池中分配一个空闲线程对象来运行提交的任务。
有几个元素组成了线程池:
等待队列:
要执行的任务队列。由于某些原因,线程池没有立即运行这些任务
芯线:
执行任务的线程对象数,由corepoolsize指定
非核心线程:
一旦任务数太大,线程池将创建非核心线程来帮助运行任务
注:不存在“核心线程”或“非核心线程”的概念。只是为了你的理解,但这是不同的。因此,两个概念都被引用。
线程池的常规工作流:
一个。开发人员提交要执行的任务。在接收到任务请求后,线程池具有以下处理条件:
当在frontline池中运行的线程数未达到核心池大小时,无论先前创建的线程是否空闲,线程池都将创建一个新线程来执行提交的任务。
当前处理池中运行的线程数达到核心池大小时,线程池将向等待队列添加任务,直到某个线程空闲,线程池将根据我们设置的等待队列规则从队列中取出一个新任务执行。
根据队列规则,此任务不能添加到等待队列中。此时,线程池创建一个“非核心线程”来直接运行任务。
注意,如果在这种情况下成功执行任务,则当前线程池中的线程数必须大于corepoolsize。
如果核心线程无法直接执行任务,无法加入等待队列,并且无法创建非核心线程,则线程池将根据拒绝处理器定义的策略处理该任务。
例如,在ThreadPoolExecutor中,如果未为线程池设置rejectedexecutionhandler。
此时,线程池将抛出rejectedexecutionexception异常,即线程池拒绝接受任务。
实际上,引发rejectedexecutionexception异常的操作是ThreadPoolExecutor线程池中的默认rejectedexecutionhandler实现。
2。一旦线程池中的线程完成执行任务,它将尝试从任务等待队列获取下一个等待任务(所有等待任务都实现BlockingQueue接口,这是一个可阻止的队列接口)。它将调用等待队列的轮询方法并停留在那里。
三个。当线程池中的线程数超过您设置的corepoolsize参数时,当前线程池中存在所谓的“非核心线程”。线程处理完任务后,如果在等待keepalivetime时间后没有分配新任务,则线程将被回收。当线程池回收线程时,它不会回收所谓的“非核心线程”,而是空闲时间达到keepalivetime阈值的线程,在线程池中的线程数等于您设置的corepoolsize参数之前,回收过程不会停止。
所谓的“核心线程”和“非核心线程”被同等对待。在进程池中的线程数等于您设置的corepoolsize参数之前,回收进程不会停止。
Executor 框架基本组成
使用 ThreadPoolExecutor 类的构造方法可以创建不同配置的线程池,但平时却很少使用。 大多数应用场景下,使用Java并发包中的 Executors 提供的5个静态工厂方法就足够了。
首先,来看看 Executor 框架的基本组成:
Executor是一个基本接口,只有一个execute(runnable)方法用于任务执行。它屏蔽了许多不相关的细节,如任务提交、线程创建和调度。
executorservice接口更加完善,提供了一些管理功能,如关闭线程池的关闭方法;还提供了更全面的任务提交机制,如使用submit方法提交任务,返回未来对象以获取任务执行结果;它甚至还具有批处理任务的功能,例如invokeall或invokeany和其他方法。
ThreadPoolExecutor、scheduledthreadpoolexecutor和forkjoinpool是java为满足复杂多变的应用场景而提供的几种基本线程池实现。
Executors是一个工具类,它提供各种静态工厂方法,从简化使用的角度创建具有不同配置的线程池。
使用ThreadPoolExecutor创建线程池
前面的内容给出了ThreadPoolExecutor的构造方法。这里,将详细解释构造方法的最后三个参数,以帮助您更好地理解和使用线程池。
线程池等待队列
只要实现了BlockingQueue接口的队列,就可以作为线程池的等待队列。例如,arrayblockingqueue、linkedblockingqueue、synchronousque、priorityblockingqueue、linkedtransferqueue等等。至于每个队列的区别和原理,我们将不在本文中讨论。
但互联网上的一些内容确实具有误导性。
Synchronousqueue也可以存储数据,它是一个无锁实现,但是size()方法直接返回0。因此,在比较synchronousqueue、linkedblockingqueue和linkedtransferqueue时,需要确认一些在线内容的正确性,这需要读者的关注。
Priorityblockingqueue将根据优先级对内部元素进行排序,优先级最高的元素将始终位于队列的头部。但是,它不能保证对具有相同优先级的元素进行排序,也不能保证当前队列中的元素(具有最高优先级的元素除外)的顺序正确。因此,这不是一个真正的命令。
线程池线程工厂
线程池最重要的任务之一是在一定的条件下创建线程。在ThreadPoolExecutor线程池中,创建线程的任务留给threadfactory。要使用线程池,必须指定threadfactory。如果未指定,则使用默认的threadfactory:defaultthreadfactory(此类位于executors工具类中)。
线程池拒绝策略
ThreadPoolExecutor线程池中还有一个重要接口:rejectedexecutionhandler。当任务提交到线程池时,线程池将拒绝处理该任务,并在以下情况下触发创建线程池时定义的拒绝策略:
新任务不能由线程池中的“核心线程”直接处理,也不能加入等待队列,也不能创建新线程来执行
线程池调用了shutdown方法以停止工作
线程池未处于正常工作状态
实际上,rejectedexecutionhandler接口有四种实现,可以直接在ThreadPoolExecutor中使用:
呼叫策略:
直接在非线程池外部调用此任务的run方法
丢弃策略:
在没有任何提示的情况下放弃被拒绝的任务
丢弃策略:
放弃等待队列头的任务,并将当前被拒绝的任务提交给线程池执行
中止策略:
拒绝任务并引发rejectedexecutionexception异常
其中,callerRunPolicy直接调用任务的run方法,可能导致线程安全问题;discardpolicy默默忽略被拒绝的任务,不输出日志或任何提示,开发人员无法知道线程池处理中的错误;discardoldestpolicy看起来很科学,但是,如果等待队列中存在容量问题,许多任务将被直接丢弃。此时,业务将出现bug,但开发人员很难找到它。
因此,更科学的方法是abortpolicy提供的处理方法:抛出异常并让开发人员处理它。当然,在特殊情况下,我也建议使用自定义拒绝策略。您可以缓存要重新发现的任务,也可以向MQ发送消息以通知业务端。
展开ThreadPoolExecutor线程池
ThreadPoolExecutor中提供了三个方法用于子类重写。它们可以帮助处于联机流程池处理任务的不同阶段的开发人员执行其他业务处理操作:
执行前:
当线程池即将开始执行任务时,线程池将触发此方法的调用。
执行后:
当线程池完成任务的执行时,线程池将触发此方法。
结束:
当线程池本身停止执行时调用此方法。
execute和submit方法的区别
ThreadPoolExecutor提供了两个方法execute和submit来提交任务,其中:
执行:提交的任务实现runnable接口。任务没有任何返回值。因此,无法获得执行结果。
提交:提交的任务实现可调用接口。任务完成后,返回执行结果。
当然,submit方法也可以提交任务来实现runnable接口,但它的处理方式与execute方法完全不同:submit方法提交的任务来实现runnable接口,将封装在executors.callable方法在线程内创建的runnableadapter对象中池,runnableadapter从可调用的继承。
线程池实践
了解执行器创建的线程池
如果使用执行器创建线程池,请确保了解每个方法创建的线程池的配置。
例如,对于newcachedthreadpool方法创建的线程池,其corepoolsize=0,maximumpoolsize=integer.max,而其等待队列是synchronousqueue,因此无法缓冲数据。它将尝试缓存线程并重用它们。当没有可用的缓存线程时,将创建一个新的工作线程。如果一个线程空闲超过60秒,它将被终止并移出缓存。当它长时间空闲时,这个线程池不会消耗任何资源。但是,需要注意的是,任务提交速度过快不仅会导致线程数量急剧增加,还会增加程序oom的风险。
在newfixedthreadpool(int nthreads)方法中,corepoolsize=maximumpoolsize=nthreads。任何时候最多有n个线程处于活动状态,这意味着如果任务数超过活动队列数,则工作队列中将出现空闲线程;如果工作线程退出,则将创建新的工作线程以补充指定的n个线程数。
在实际的应用场景中,许多人可能会滥用这些方法。因此,Alibaba Java规范建议使用ThreadPoolExecutor构造方法而不是executors。
不要随意创建线程池
从应用程序或服务的角度出发,我们可以对整个服务中线程的用途进行分类,并为每个分类创建适当的线程池。
我看过很多代码。只要使用线程,执行器就用来创建thr