jjzjj

Java 线程池之ThreadPoolExecutor学习总结

授客的博客 2023-04-16 原文

前提

java version "1.8.0_25"

池简述

软件开发活动中,我们经常会听到数据库连接池、内存池、线程池等各种“池”概念,这些“池”到底是什么东西呢?程序的世界里,我们可以将池简单的理解为一种容器类数据结构,比如列表。程序处理信息的过程中,可能会依赖某些资源或者对象(暂且统一称之为对象),比如数据库连接,来执行一些高频操作,比如数据表查询,此时,如果被依赖对象的存活时间比较短,那就意味着需要频繁的创建和销毁对象,这可能会很耗时、耗系统资源(CPU、内存、磁盘、网络等)。为了解决这个问题,进行程序设计时,可能会考虑在程序初始化时,预先创建一批所需对象,并存储到池中,或者根据需要即时创建对象,并在使用完成后,将对象添加到池中,这样,当程序需要(再次)使用对象时,可以直接从池中直接获取现有的对象,节省了频繁创建和销毁对象带来的资源浪费,这就是池的作用,为程序提供复用对象或者提前分配资源的能力。

ThreadPoolExecutor线程池介绍

下文仅针对线程池的一些要点做介绍

任务处理流程

核心线程池大小(corePoolSize)和最大线程池大小(maximumPoolSize)

ThreadPoolExecutor会根据corePoolSize(保持存活(不允许超时退出等)的最小工作线程数,如果设置了allowCoreThreadTimeOuttrue,则该值为0。可通过getPoolSize方法获取该值) 和maximumPoolSize(线程池中允许的最大线程数,可通过getMaximumPoolSize获取该值)设置的界限自动调整线程池的大小。

当通过execute(Runnable) 方法提交新任务后,如果正在运行的线程的数量小于corePoolSize,则创建新线程来处理请求,即使存在其它空闲的工作线程,否则如果正在运行的线程的数量大于corePoolSize,但小于maximumPoolSize,则仅仅在队列已经满时才会创建新线程来处理请求。设置corePoolSize等于maximumPoolSize则表示创建一个固定大小的线程池。

通过设置maximumPoolSize为基本无界的值,例如Integer.MAX_VALUE,则允许线程池容纳任意并发任务数。大多数情况下,corePoolSizemaximumPoolSize仅在构建时设置,但也可以分别用使用setCorePoolSizesetMaximumPoolSize对其进行动态更改。

按需创建线程

默认情况下,仅在新任务到达时创建和启动线程,即便是核心线程。可以使用prestartCoreThread或者prestartAllCoreThreads对此进行动态更改。如果使用非空队列构造线程池,你可能会想预先启动线程。

创建新线程

使用ThreadFactory创建新线程。如果未指定,则使用Executors.defaultThreadFactory,其创建的线程都位于相同线程组,且拥有相同的优先级NORM_PRIORITY以及非守护状态。通过提供不同的线程工程ThreadFactory,可以修改线程的名称,线程组,优先级,守护状态等等。当newThread返回null时,ThreadFactory将无法创建线程,此时执行器继续运行,但是可能无法执行任何任务。线程应该拥有modifyThread RuntimePermission。如果工作线程或者其它线程使用不具有该权限的线程池,服务可能被降级:配置变更可能不会及时生效,且关闭线程池可能会保留终止但未完成的状态。

线程保持存活时间

如果线程池当前拥有多于corePoolSize数量的线程,则空闲时间超过keepAliveTime(可通过getKeepAliveTime(TimeUnit)方法获取)的线程将被终止,以减少资源消耗。可以通过setKeepAliveTime(long,TimeUnit)方法动态改变该参数值。使用setKeepAliveTime(Long.MAX_VALUE, NANOSECONDS)可以有效的阻止空闲线程在关闭之前终止。默认情况下,keep-alive策略仅在线程池中线程数多余corePoolSize时起作用。keepAliveTime的值不为0的情况下,可通过allowCoreThreadTimeOut(boolean)方法将keep-alive策略应用于核心线程。

排队(Queuing)

BlockingQueue用于传输和容纳提交的任务。此队列的使用与线程池大小变化相关:

  • 如果线程池中当前线程数少于corePoolSize,那么Executor总是优先创建新线程来处理任务请求,而不是让任务请求排队
  • 如果线程池中当前线程数等于或者多余corePoolSize,那么Executor总是优先让任务排队,而不是创建新线程
  • 如果无法让任务请求排队(比如任务队列已满),且线程池中当前线程数未超过maximumPoolSize,则创建一个新线程来处理任务请求,否则将拒绝该任务请求

三种排队策略:

  • 直接传递(Direct handoffs)

    SynchronousQueue是工作队列(workQueue)的一个默认好选择。它将任务交给线程,而不是保留它们。此时,如果没有立即可用的线程,将构造新线程,因为让任务排队的尝试将会失败。此策略在处理可能具有内部依赖关系的请求集时避免锁定。通常需要无界的maximumPoolSize,以避免拒绝新任务的提交。这反过来说明当任务平均提交速度持续大于平均处理速度时,线程数无限增长的可能性。如果使用newCachedThreadPool创建线程池则表示使用直接传递策略

  • 无界队列(Unbounded queues)

    当所有核心线程都繁忙时,使用无界队列(例如,没有预定义容量的LinkedBlockingQueue)将导致新任务在队列中等待,从而导致没有多余corePoolSize的线程被创建(maximumPoolSize的值不起任何作用)。当每个任务完全彼此独立,互不影响执行时,这可能是合适的。例如,在网页服务器中, 这种排队方式用于平滑瞬时大量请求时很有用。需要注意的是,当任务平均提交速度持续大于平均处理速度时,可能会导致无界队列无限增长。如果使用newFixedThreadPool 创建线程池则表示使用无界队列。

  • 有界队列(Bounded queues)

    有界队列(例如,ArrayBlockingQueue)配合maximumPoolSizes使用有助于防止资源耗尽,但是难以调整和控制。队列大小和最大线程池大小需要相互权衡:使用大队列和较小的线程池可以最大限度地减少CPU使用率,操作系统资源和上下文切换开销,但是会导致人为的低吞吐量。如果任务频繁被阻塞(比如I/O限制),那么系统可以调度比我们允许的更多的线程。使用小队列通常需要较大的线程池,这会让CPU保持繁忙,但可能会产生不可接受的调度开销,这也会降低吞吐量。

拒绝处理任务

Executor已关闭、使用有界的线程池、工作队列,且达到最大值时,通过方法execute(Runnable)提交的任务将被拒绝。在任何一种情况下,execute方法调用其RejectedExecutionHandlerrejectedExecution(Runnable,ThreadPoolExecutor)方法。提供以下4种预定义处理策略:
ThreadPoolExecutor.AbortPolicy(默认策略)
拒绝任务时,处理器会抛出一个运行时异常RejectedExecutionException

ThreadPoolExecutor.CallerRunsPolicy
调用execute的线程自己运行任务。这提供了一个简单的反馈控制机制,将会降低新任务提交的速率。

ThreadPoolExecutor.DiscardPolicy
不能被执行的任务将被抛弃

ThreadPoolExecutor.DiscardOldestPolicy
如果Executor已关闭,工作队列队首的任务被丢弃,然后重试执行。(重试也可能失败,导致重复执行前面的动作)

可以定义和使用其他类型的RejectedExecutionHandler类。这样做需要一些谨慎,特别是当策略被设计为仅在特定容量或者队列策略下有效时

线程运行状态

该线程池使用了一个runState来对线程进行主要生命周期控制,具有以下值:

RUNNING: 接收新任务并且处理排队的任务
SHUTDOWN: 不接收新任务,但是处理排队的任务。
STOP: 不接收新任务,不处理排队的任务,并且中断正在进行的任务。
TIDYING: 所有任务已终止。workerCount为0。线程转为TIDYING状态将会运行terminated() hook方法。
TERMINATED: terminated()已经运行完。

这些值之间的数字顺序很重要,为了支持有序比较,runState会随着时间单调递增,但不需要达到每个状态。

状态转换如下:
RUNNING -> SHUTDOWN
调用shutdown()时,可能隐式的在finalize()中调用

RUNNING 或者 SHUTDOWN -> STOP
调用shutdownNow()

SHUTDOWN -> TIDYING
当工作队列和线程池都为空时
STOP -> TIDYING
线程池为空时

TIDYING -> TERMINATED
terminated() hook方法运行完成时。

线程的析构(Finalization)

如果线程池不再被程序引用且没有剩余的线程,线程池将被关闭。如果希望确保未被引用的线程池被回收,即使用户用户忘记调用shutdown,则必须通过适当的keep-alive配置,使用更低的下限--0核心线程数或者设置allowCoreThreadTimeOut(boolean),确保未使用的线程最终会消亡。

有关Java 线程池之ThreadPoolExecutor学习总结的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  6. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  7. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  8. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  9. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  10. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

随机推荐