Java线程池内核心线程数、饱和策略都是什么?该怎么配置呢?

gentle_zhou 发表于 2022/05/17 16:24:44 2022/05/17
【摘要】 本文内容会分为:线程池介绍、线程池处理流程、线程池构造函数、核心线程数和最大线程数的配置、饱和策略的配置。

日常在Java多线程项目开发过程中,我们经常会用到this.executorService = new ThreadPoolExecutor(XXXXX)函数来创建一个新的线程池实例,接着使用executorService.execute(runnable) 或则 executorService.submit(runnable)来执行任务。但是线程池其中涉及到的核心线程数、饱和策略,我们都该怎么配置呢?不能不理解原理,每次都直接int corePoolSize = Runtime.getRuntime().availableProcessors();就算配置好了吧…

本文内容会分为:线程池介绍、线程池处理流程、线程池构造函数、核心线程数和最大线程数的配置、饱和策略的配置。

线程池介绍

An ExecutorService that executes each submitted task using one of possibly several pooled threads, normally configured using Executors factory methods.

一个线程池执行器服务,通常由执行器工厂方法来配置,会分配池内某一个线程去执行每个提交的任务。而由ThreadPoolExecutor线程池执行器创建的实例就是Java项目里的线程池了。

顾名思义,线程池就是管理一系列线程的资源池;其内部还会有一个保存了所有等待执行任务的阻塞队列,任务也很简单:当有空闲的线程时,从该阻塞队列里获取一个任务分配给这个线程去执行。

线程池的优势也比为每个任务分配一个新线程更明显:

  1. 通过重复利用已经创建的线程来降低资源的消耗(不需要每次都创建、销毁线程);
  2. 当有空闲的线程的时候,可以直接去执行任务,而不需要等待线程创建好再去执行,提高了任务的响应速度;
  3. 线程池可以对线程进行统一的管理、分配和监控。

线程池处理流程

image.png


从上图我们可以看出,当一个任务分配到线程池内,线程池主线程会去调用execute()方法,然后会有4种情况:

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到阻塞队列里等待执行
  3. 如果情况1和2都试过了,无法将任务添加到阻塞队列里,但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution()方法

置于线程池如何新建和执行线程呢?
新建线程:通过线程工厂方法去创建线程,在ThreadFactory中只定义了一个方法newThread(),用于创建新线程:
image.png

执行线程:涉及到executorServiceexecute()submit() 方法了;前一个方法会去执行参数里数据类型为Runnable的command任务,后一个方法则是继承自java.util.concurrent.AbstractExecutorService类-提交一个Runnable任务去执行并返回一个表示该任务的Future对象:
image.png

image.png

线程池构造函数

我们接着看看线程池的构造函数里面都有哪些具体的参数呢?
image.png

  1. int corePoolSize, 核心线程数量
  2. int maximumPoolSize, 最大线程数量
  3. long keepAliveTime, 核心线程除外的空闲线程可以存活的最大时间
  4. TimeUnit unit, 对应于keepAliveTime的时间单位
  5. BlockingQueue<Runnable> workQueue, 阻塞队列,当核心线程数全部都被使用,里面存放当前处理不了等待执行的任务
  6. ThreadFactory threadFactory, 负责创建线程的线程工厂
  7. RejectedExecutionHandler handler, 饱和策略也叫拒绝策略

这里重点讲一下阻塞队列和线程工厂(核心线程数、最大线程数和饱和策略会在下面单独讲):

  • 阻塞队列:就像前面说的,“如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到阻塞队列里等待执行”,队列里可以把这些新到的请求存储起来,等待空闲的线程。常见的阻塞队列有以下几种:
    1.ArrayBlockingQueue:数组形式的工作队列,有界队列,先进先出
    2.LinkedBlockingQueue:链表形式的工作队列,有界/无界队列,先进先出
    3.SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制;要将一个元素放入SynchronousQueue中, 必须有另一个线程正在等待接受这个元素,也就是该“队列”并不会保存任何任务
    4.PriorityBlockingQueue:优先级队列,有界队列,根据优先级来安排任务
    5.DelayedWorkQueue:延迟的工作队列,无界队列
  • 线程工厂:用来创建线程的工厂,我们可以通过线程工厂给每个创建的线程设置有意义的名字以及设置为是否是守护线程。默认的线程工厂有2种:DefaultThreadFactory和PrivilegedThreadFactory,或则我们可以通过ThreadUtil.newNamedThreadFactory(XXX)来自定义。
    注:守护线程是指为其他线程服务的线程;在JVM中,JVM退出时,不必关心守护线程是否已结束,只要所有非守护线程都执行完毕了,虚拟机就会自动退出。

接下去我们开始讲如何配置核心线程数、最大线程数和饱和策略。

核心线程数和最大线程数的配置

ThreadPoolExecutor会根据corePoolSizemaximumPoolSize设置的界限来自动调整池的大小。

那么我们如何配置核心线程数和最大线程数呢?

首先我们通过Runtime.getRuntime().availableProcessors();获得当前机器的CPU核数(很多时候开发就直接把这个CPU核数作为核心线程数了)。

接着判断一下线程池处理的程序是CPU密集型还是IO密集型。如何判断呢?CPU密集型:计算密集,CPU有许多运算要处理,需要读写IO(硬盘/内存),IO却可以在很短的时间就完成,CPU Loading很高。IO密集型:和CPU密集型相反,系统的CPU性能相对硬盘、内存要好很多,大部分的状况是CPU在等IO (硬盘/内存) 的读写操作,CPU Loading不高。

理论上来说:
如果是CPU密集型:核心线程数 = CPU核数 + 1;
而如果是IO密集型:核心线程数 = CPU核数 * 2。
而具体如何配置,还是需要通过压力测试在上述理论值范围浮动确定;不同的机器环境都会导致实际数值不同。

注:corePoolSizemaximumPoolSize的默认值是1和Integer.MAX_VALUE。

饱和策略的配置

饱和策略,也叫做拒绝策略,由RejectedExecutionHandler任务拒绝处理器来执行拒绝任务。

两种情况会触发线程池的饱和策略来拒绝处理新任务:

  1. 当目前正在使用的线程数已经达到了最大线程数,且workQueue阻塞队列已满,会拒绝新任务
  2. 当线程池被调用了shutdown()功能,就会等待当前线程池里已经在执行的任务执行完毕,再真正的shutdown;在此间隔期内,会拒绝新任务

有如下4种预先定义好的饱和策略:

  1. 饱和策略默认采用ThreadPoolExecutor.AbortPolicy;在触发到任务拒绝处理器的时候,会直接抛出运行时RejectedExecutionException(可以被调用者捕获并做后续处理)
  2. ThreadPoolExecutor.CallerRunsPolicy,使用调用者所在的线程去执行新任务,不抛弃任务也不会抛出异常;它不会使用线程池内某个线程去执行新任务,而是在调用了execute的线程(也就是主线程)内执行该任务
  3. ThreadPoolExecutor.DiscardPolicy,新分配的任务会被直接抛弃,且不抛出异常
  4. ThreadPoolExecutor.DiscardOldestPolicy,使用poll()方法抛弃工作队列里最旧的那个任务(也就是本来下一个要去执行的任务),然后尝试重新提交这个任务(可能再次失败,导致重复执行下去)

注1:CallerRunsPolicy虽然名义上不会抛弃任务,会让调用了execute的线程去执行任务;但我们想一下,由于执行任务需要一定时间,在这段时间内,新来的任务不会被线程池的主线程accept,此时这些任务请求将会被保存在TCP层的队列里。如果这个时间过长,且请求过多持续保存在TCP层队列里,那么该TCP层队列也会被填满导致后续请求被抛弃。

注2:DiscardOldestPolicy策略里阻塞队列如果是优先队列,那么这里的最旧会是优先级最高的任务,所以这个策略不要和优先级队列一起使用为好。

当然除了这4种预先定义好的策略以外,我们还可以使用用户自定义的拒绝策略,开发实现RejectedExecutionHandler并自己定义策略模式。

参考链接

  1. https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
  2. https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/AbstractExecutorService.html#submit(java.lang.Runnable)
  3. https://developpaper.com/threadpoolexecutor-thread-pool-analysis/
  4. https://www.liaoxuefeng.com/wiki/1252599548343744/1306580788183074
  5. https://www.tabnine.com/code/java/methods/cn.hutool.core.thread.NamedThreadFactory/<init>
  6. https://zhuanlan.zhihu.com/p/34405230
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。