当前位置: 主页 > JAVA语言

java线程池最大线程数-java线程池原理

发布时间:2023-02-13 10:13   浏览次数:次   作者:佚名

每篇文章一句话

烧不死的鸟就是火凤凰,烧死了就会变成烤鸽子。

1 概述

在Java中,我们通常通过集成Thread类并实现Runnnable接口,调用线程的start()方法来启动线程。 但是如果并发量很大,每个线程执行很短时间就结束了,这种频繁的线程创建和进程销毁会大大降低系统运行的效率。 线程池的产生是为了解决多线程效率低下的问题。 它允许线程被重用,即线程执行后不被销毁,而是可以继续执行其他任务。 (这里可以以tomcat为例来思考)

很多人要问,线程池听上去高大上,实际工作中却很少用到。 事实上,池化技术在各种流行的框架或高性能架构中无处不在。 那么有人要问了java线程池最大线程数,线程池有什么用呢?

一句话,就是提高系统效率和吞吐量。 如果服务器为每个请求创建一个线程,那么短时间内会发生很多创建和销毁动作。 然而,服务器在创建和销毁线程上花费大量时间和消耗系统资源。 线程池可以尽量减少这种情况的发生。

所以,java.util.concurrent.ThreadPoolExecutor类(java5之后出现,由大师Doug Lea完成),不得不说,它是今天的主菜

2.栗子

一、ThreadPoolExecutor的重要参数

corePoolSize:核心线程数

queueCapacity:任务队列容量(阻塞队列)

maxPoolSize:最大线程数

keepAliveTime:线程空闲时间

当线程空闲时间达到keepAliveTime时,线程将被销毁,直到线程数=corePoolSize

如果allowCoreThreadTimeout=true,直到线程数=0(这个特性需要注意)

allowCoreThreadTimeout:允许核心线程超时(如上,会影响keepAliveTime)

rejectedExecutionHandler:任务拒绝处理程序(用户可以自定义拒绝后的处理方式)

两种情况会拒绝处理任务:

1.当线程数达到maxPoolSize且任务队列已满时,新任务将被拒绝

2、当线程池被调用shutdown()时,会等待线程池中的任务执行完毕后才关闭。 如果在调用 shutdown() 和线程池实际关闭之间提交了任务,则新任务将被拒绝(它不会立即停止,而是在执行后停止)。

如果被拒绝,此时线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值为AbortPolicy,会抛出异常

hreadPoolExecutor 类有几个内部实现类来处理这种情况:

1:AbortPolicy丢弃任务并抛出运行时异常

2:CallerRunsPolicy执行任务(该策略会重试添加当前任务,会自动重复调用execute()方法,直到成功)如果执行器关闭,则丢弃。

3:DiscardPolicy直接静默丢弃被拒绝的任务,无异常信息

4:DiscardOldestPolicy不丢弃被拒绝的任务,而是丢弃队列中等待时间最长的线程(队头的任务会被删除),然后将被拒绝的任务加入队列(Queue是first-in- first-out任务调度算法,具体策略下面会分析)(如果再次失败,重复该过程)

5:实现RejectedExecutionHandler接口,可以自定义处理器(可以自己实现然后设置进去)

2、ThreadPoolExecutor处理任务的顺序和原理

通过 execute(Runnable) 方法将任务添加到线程池中。 任务是一个Runnable类型的对象,任务的执行方法是Runnable类型对象的run()方法。

当通过execute(Runnable)方法向线程池添加任务时,线程池采用的策略如下(即添加任务的策略):

如果此时线程池中的数量小于corePoolSize,即使线程池中的线程空闲,也会创建新的线程来处理新增的任务。

如果此时线程池中的个数等于corePoolSize,但是缓冲队列workQueue没有满,则将任务放入缓冲队列。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue已满,线程池中的数量小于maximumPoolSizejava线程池最大线程数,则创建新的线程来处理新增的任务。

java线程池大小_java线程池原理_java线程池最大线程数

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue已满,线程池中的数量等于maximumPoolSize,则通过handler指定的策略处理任务。

任务处理的优先级(顺序)为:

如果核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize都满了,使用handler来处理被拒绝的任务。 当线程池中的线程数大于corePoolSize时,如果一个线程空闲时间超过keepAliveTime,该线程就会被终止。 这样,线程池就可以动态调整池中线程的数量。

简要总结如下:

当线程数小于核心线程数时创建线程。

当线程数大于等于核心线程数且任务队列未满时,将任务放入任务队列。

当线程数大于等于核心线程数,且任务队列满时

如果线程数小于最大线程数,则创建一个线程

如果线程数等于最大线程数,则抛出异常并拒绝任务

线程池处理流程图:

【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)_java

这里提醒一下:如果失败处理策略选择了DiscardOldestPolicy,可能会丢失任务。

另外,Executors类中还有几个方法:newFixedThreadPool()、newCachedThreadPool()等方法,实际上是间接调用了ThreadPoolExocutor,只是传递了不同的构造参数。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }
	public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

Executors.newCachedThreadPool(); //创建一个缓冲池,容量为Integer.MAX_VALUE

Executors.newSingleThreadExecutor(); //创建一个容量为1的缓冲池

Executors.newFixedThreadPool(int); //创建一个固定容量的缓冲池

ThreadPoolExecutor的继承关系如下

执行器->ExecutorService->AbstractExecutorService->ThreadPoolExecutor

其中有一个构造函数:

java线程池原理_java线程池最大线程数_java线程池大小

public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue workQueue,
                           RejectedExecutionHandler handler) {
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          Executors.defaultThreadFactory(), handler);
 }
 发现我们可以指定workQueue和handler。当然还有其余的构造函数,有类似的效果

线程池构造函数的7个参数解释:

强烈建议程序员使用更方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor() (单后台线程),它具有适用于大多数使用场景的预定义设置。

但是,但是,但是。 . . 使用 fix 有一些陷阱。 具体可以看我的博文(生产环境中的一个活生生的杀人案):

另外,这里我说一下ThreadPoolTask​​Executor:

ThreadPoolTask​​Executor是spring的线程池技术。 其实它的实现完全是使用ThreadPoolExecutor实现的(有点类似于装饰器模式,当然Spring提供的功能更强大,因为还有定时调度功能)。

3、如何设置线程池的参数:

系统默认

核心池大小=1

queueCapacity=Integer.MAX_VALUE

maxPoolSize=Integer.MAX_VALUE

keepAliveTime=60s

allowCoreThreadTimeout=false

rejectedExecutionHandler = AbortPolicy()

那我们怎么设置呢?需要根据几个值来决定

tasks : 每秒任务数,假设500~1000

taskcost:每个任务花费的时间,假设0.1s

responsetime:系统允许的最大响应时间,假设为1s

做一些计算

corePoolSize = 每秒需要多少个线程?

threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100个线程。 corePoolSize 设置应大于 50

根据8020原则,如果每秒80%的任务小于800,则设置corePoolSize为80

java线程池最大线程数_java线程池大小_java线程池原理

queueCapacity = (coreSizePool/taskcost)*响应时间

计算可以是queueCapacity = 80/1 = 80。表示队列中的线程可以等待1s,超过则需要开新线程执行

记住不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小。 当任务激增时,无法开启新的线程执行,响应时间会激增。

maxPoolSize = (max(tasks)-queueCapacity)/(1/taskcost)

计算 maxPoolSize = (1000-80)/10 = 92

(最大任务数-队列容量)/每线程每秒处理能力=最大线程数

rejectedExecutionHandler:视具体情况而定。 如果任务不重要,可以放弃。 如果任务很重要,应该使用一些缓冲机制来处理它。

keepAliveTime 和 allowCoreThreadTimeout 通常默认满足

以上为理想值,实际情况需根据机器性能而定。 如果在未达到最大线程数时机器cpu负载已经满了,就需要升级硬件(呵呵)和优化代码来降低taskcost。

JDK1.5的线程池由Executor框架提供。 Executor 框架将处理请求任务的提交与其执行分离。 可以制定执行策略。 在线程池中执行线程可以复用现有线程而不是创建新线程,这样可以抵消处理多个请求时线程创建和消亡的开销。 如果线程池太大,会导致内存占用过高,也会耗尽资源。 如果它太小,则会由于存在许多不工作的处理器资源而导致吞吐量损失。

如何合理配置线程池大小,一般需要根据任务类型配置线程池大小:

1、如果是CPU密集型任务,需要尽可能的压榨CPU。 参考值可以设置为NCPU+1(比如4核就配置为5)

2.如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体设置还需要根据实际情况进行调整。 例如,可以先设置线程池大小作为参考值,然后观察任务运行状态、系统负载、资源利用率等情况,做出适当的调整。

三、使用场景

1.当你的任务不是必须的,比如记录操作日志,通知第三方服务不需要的信息等,可以使用线程池来处理非阻塞任务

2.当你的任务很耗时的时候,可以使用线程池技术

3.当请求并发较高时,可以使用线程池技术优化处理

线程池可以通过 Executors 静态工厂来构建,但一般不推荐这样做。

提醒:当线程池可以使用的时候,不要自己去开启新的线程。 在高并发环境下,系统资源是宝贵的,需要节约资源来提高可用性。

复活节彩蛋

Executors 工具类为我们提供了许多快速创建线程池的方法,尽管我们不推荐使用它们。 但是里面有一个方法我觉得还不错:Executors.newSingleThreadExecutor() 如果用这个作为全局线程池,可以很好的实现异步,同时也可以保证任务的顺序执行,从而实现消峰效果:

        private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
    private static AtomicInteger num = new AtomicInteger();
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                num.getAndIncrement();
                System.out.println(Thread.currentThread().getName() + "-->>>>" + num);
            });

java线程池原理_java线程池最大线程数_java线程池大小

} executorService.shutdown(); } 输出: pool-1-thread-1-->>>>1 pool-1-thread-1-->>>>2 pool-1-thread-1-->>>>3 pool-1-thread-1-->>>>4 pool-1-thread-1-->>>>5 pool-1-thread-1-->>>>6 pool-1-thread-1-->>>>7 pool-1-thread-1-->>>>8 pool-1-thread-1-->>>>9 pool-1-thread-1-->>>>10 pool-1-thread-1-->>>>11 pool-1-thread-1-->>>>12 pool-1-thread-1-->>>>13 pool-1-thread-1-->>>>14 pool-1-thread-1-->>>>15 pool-1-thread-1-->>>>16 pool-1-thread-1-->>>>17 pool-1-thread-1-->>>>18 pool-1-thread-1-->>>>19 pool-1-thread-1-->>>>20

我们发现只有一个pool,也只有一个线程,任务是顺序执行的。 我们在使用Fiexed的时候,也可以达到顺序执行的效果。 因为内部阻塞队列是FIFO的一种实现:

    private static final ExecutorService executorService = Executors.newFixedThreadPool(5);
    private static AtomicInteger num = new AtomicInteger();

java线程池最大线程数_java线程池原理_java线程池大小

public static void main(String[] args) { for (int i = 0; i < 20; i++) { executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + "-->>>>" + num.getAndIncrement()); }); } executorService.shutdown(); } 输出: pool-1-thread-2-->>>>0 pool-1-thread-1-->>>>1 pool-1-thread-2-->>>>2 pool-1-thread-1-->>>>3 pool-1-thread-2-->>>>4 pool-1-thread-1-->>>>5 pool-1-thread-2-->>>>6 pool-1-thread-1-->>>>7 pool-1-thread-2-->>>>8 pool-1-thread-1-->>>>9 pool-1-thread-2-->>>>10 pool-1-thread-1-->>>>11 pool-1-thread-2-->>>>12 pool-1-thread-1-->>>>13 pool-1-thread-2-->>>>14 pool-1-thread-1-->>>>15 pool-1-thread-2-->>>>16 pool-1-thread-4-->>>>17 pool-1-thread-3-->>>>18 pool-1-thread-5-->>>>19

【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)_ThreadPoolExecutor_02