设置动态线程池参数原理与实践

简介: 设置动态线程池参数原理与实践

本文主要介绍Java线程池的原理,涉及源码方面的分析,并最终实现动态设置线程池中主要参数的案例。

线程池的是利用池化思想设置的管理多线程的工具,其主要的优势是:(1)减少创建线程和销毁线程的资源开销;(2)多个线程并行处理能有效提升多任务处理的效率;(3)可以自定义线程池中的参数,有良好的可拓展性。本文重点针对动态设置线程池中参数进行说明和演示。

一、线程池原理

Java中线程池的继承关系,其中Executor是顶层接口,规定了线程池的最基本的execute()方法,其继承的ExecutorService拓展了对线程池的操作的方法,其主要的实现类是ThreadPoolExecutor,也是常见的创建线程池的实现类。

1.线程池参数含义

以下说明线程池实现类ThreadPoolExecutor中参数的含义:

int corePoolSize,//核心线程池的大小 
int maximumPoolSize,//最大线程池的大小 
long keepAliveTime,//存活时间 
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//工作队列
ThreadFactory threadFactory,//线程产生工厂
RejectedExecutionHandler handler)//拒绝策略

设置线程池参数的策略

主要需要考虑的是设置corePoolSize、maximumPoolSize、workQueue参数,业界常规的设置策略:

CPU密集型:corePoolSize = CPU核数 + 1

IO密集型:corePoolSize = CPU核数 * 2

2.线程池任务调度逻辑

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
    其流程图如下:

    其中ThreadPoolExecutor#execute()方法的处理逻辑:
public void execute(Runnable command) { 
    //1、判断是否传进来线程 
    if (command == null) 
        throw new  NullPointerException(); 
    int c = ctl.get(); 
    //2、判断工作线程池是否满了 
    if (workerCountOf(c) < corePoolSize) { 
        if (addWorker(command, true)) 
            return; 
        c = ctl.get(); 
    } 
    //3、判断工作队列是否满了 
    if (isRunning(c) && workQueue.offer(command)) { 
        int recheck = ctl.get(); 
        if (! isRunning(recheck) && remove(command)) 
            reject(command);
        else if (workerCountOf(recheck) == 0) 
            addWorker(null, false);
    } 
    //4、以上条件都不符合,直接拒绝 
    else if (!addWorker(command, false)) 
        reject(command);
}

二、动态设置线程池参数

在日常实践过程中,一次性就能确定线程池的参数其实是比较困难,所以就需要能够动态设置线程池的参数。以下是几种常见的动态设置线程池参数的方法。

public void setCorePoolSize(int corePoolSize); //设置核心线程数
public void setMaximumPoolSize(int maximumPoolSize); //设置最大线程数
public void setKeepAliveTime(long time, TimeUnit unit); //设置空闲存活时间
public void setThreadFactory(ThreadFactory threadFactory); //设置线程工厂
public void setRejectedExecutionHandler(RejectedExecutionHandler handler); //设置拒绝策略

动态设置线程池参数方法

调整核心线程数和最大线程数的

executor.setCorePoolSize(corePoolSize + size);
executor.setMaximumPoolSize(maxPoolSize + size);

调整线程池阻塞队列大小

原始LinkedBlockingQueue的capacity是final类型,无法被修改。

/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;

可以通过重写LinkedBlockingQueue成ResizeableCapacityLinkedBlockingQueue,将capacity的final修饰符去掉,实现setter/getter方法,这样就能正常修改了。

private int capacity;
public int getCapacity() {
    return capacity;
}
public void setCapacity(int capacity) {
    this.capacity = capacity;
}

1.具体实践

之前工作中碰到一个场景,就是正常情况下使用默认的线程池参数执行多线程任务可以正常执行,但在某些特殊情况下发的任务执行时间远远超过任务超时执行时间,所以当执行这类任务的时候就会报错。针对这种情况,当时考虑使用动态设置线程池的方案,即在正常情况下使用默认线程池执行,在运行特殊任务的时候将默认线程池进行对应任务数的弹性扩容,这样就不会影响到正常的任务执行,但该类任务执行完成后,再将线程数设置为默认值。

void noVmExeperiment(int size) {
    int corePoolSize = executor.getCorePoolSize();
    executor.setCorePoolSize(corePoolSize + size);  //执行该类任务时动态扩容核心线程数
    for (int i = 0; i < size; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(">>>>>>>>" + Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    executor.setCorePoolSize(corePoolSize); //执行完成后核心线程数回退到默认值
}

代码地址:https://github.com/yangnk/SpringBoot_Learning/tree/master/SpringBootExample/src/main/java/com/yangnk/dynamicThreadPool

2.常见问题

当设置的corePoolSize大于原maximumPoolSize,其最大的工作线程不会达到corePoolSize,只会是maximumPoolSize。比如原线程corePoolSize是3,maximumPoolSize是10,经过动态调整后corePoolSize为20,其实际最大工作线程也只会是10。

究其原因这是由于hreadPoolExecutor#getTask()方法中执行的逻辑是:创建新的工作线程会将工作线程+1,当但工作线程超过maximumPoolSize时会将工作线程-1,这样一来工作线程值停留在maximumPoolSize。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //当工作线程数量超过最大线程数时,减少工作线程数
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

TODO

  • 还需要再详细说明线程池的源码;

参考资料

  1. Java线程池实现原理及其在美团业务中的实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
  2. 动态线程池的简单实现思路:https://juejin.cn/post/7240065163486216253
  3. 线程池监控和动态配置:https://juejin.cn/post/7104814252510150692
  4. Java并发编程学习篇8_基于开源的配置中心的轻量动态线程池dynamic-tp实践与源码原理分析:https://blog.csdn.net/qq_24654501/article/details/125503922?spm=1001.2014.3001.5501
  5. 线程池中各个参数如何合理设置:https://blog.csdn.net/riemann_/article/details/104704197
  6. 填个坑!再谈线程池动态调整那点事。:https://segmentfault.com/a/1190000040858637 (动态调整线程池参数)
  7. 如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。:https://mp.weixin.qq.com/s/YbyC3qQfUm4B_QQ03GFiNw


目录
相关文章
|
17天前
|
数据采集 存储 Java
高德地图爬虫实践:Java多线程并发处理策略
高德地图爬虫实践:Java多线程并发处理策略
|
20天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
50 0
|
7天前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第5天】在现代软件开发中,多线程编程是一个重要的概念,尤其是在Java这样的多平台、高性能的编程语言中。通过多线程,我们可以实现并行处理,提高程序的运行效率。本文将介绍Java中多线程编程的基础知识,包括线程的概念、创建和控制方法,以及一些常见的多线程问题和解决方案。
|
11天前
|
Java 调度 开发者
Java中的多线程编程:基础与实践
【5月更文挑战第2天】本文将深入探讨Java中的多线程编程,从基础概念到实际应用,为读者提供全面的理解和实践指导。我们将首先介绍线程的基本概念和重要性,然后详细解析Java中实现多线程的两种主要方式:继承Thread类和实现Runnable接口。接着,我们将探讨线程同步的问题,包括synchronized关键字和Lock接口的使用。最后,我们将通过一个实际的生产者-消费者模型来演示多线程编程的实践应用。
|
11天前
|
安全 Java 程序员
Java中的多线程编程:从理论到实践
【5月更文挑战第2天】 在计算机科学中,多线程编程是一项重要的技术,它允许多个任务在同一时间段内并发执行。在Java中,多线程编程是通过创建并管理线程来实现的。本文将深入探讨Java中的多线程编程,包括线程的概念、如何创建和管理线程、以及多线程编程的一些常见问题和解决方案。
19 1
|
12天前
|
并行计算 Java 数据处理
Java中的多线程编程:基础知识与实践
【5月更文挑战第1天】本文将深入探讨Java中的多线程编程,包括其基本概念、实现方式以及实际应用。我们将从理论和实践两个角度出发,详细解析线程的创建、启动、控制以及同步等关键问题,并通过实例代码演示如何在Java中有效地使用多线程。
|
12天前
|
Java 程序员
Java中的多线程编程:从理论到实践
【5月更文挑战第1天】 在现代计算机科学中,多线程编程是一个重要的概念,它允许程序员在同一程序中并行运行多个任务。Java作为一种广泛使用的编程语言,提供了一套丰富的多线程编程工具。本文将介绍Java中多线程编程的基本概念,包括线程的创建、启动、控制和同步,以及一些常见的多线程问题和解决方案。
|
12天前
|
存储 Java 程序员
Java中的多线程编程:基础知识与实践
【5月更文挑战第1天】在现代计算机科学中,多线程是一种重要的并行计算技术,允许多个执行流程并发运行。本文将深入探讨Java语言中的多线程编程,从基础概念到实际应用,帮助读者理解多线程的核心原理,并通过实例学习如何在Java中创建和管理线程。我们将涵盖线程的生命周期、同步机制以及如何利用高级类如Executor框架来优化多线程应用的性能。通过本文的学习,读者将具备设计和实现高效、稳定多线程Java应用程序的能力。
7 2
|
12天前
|
Java 调度 开发者
Java中的多线程编程:基础知识与实践
【4月更文挑战第30天】 在现代软件开发中,多线程编程是提高程序性能和响应能力的关键。Java作为一款广泛使用的编程语言,提供了丰富的多线程支持。本文将介绍Java多线程的基础概念、实现方法以及常见问题的解决策略。我们将从线程的创建和管理入手,逐步深入到同步机制、死锁避免以及高级并发工具类的应用。通过实例代码演示和理论分析,旨在帮助读者掌握Java多线程编程的核心技能,提升软件项目的并行处理能力。
|
12天前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践