线程池深入分析:参数设计优化和避坑指南

一、线程池参数为什么如此重要?

在高并发场景下,线程池的参数配置直接影响系统的吞吐量、响应时间和稳定性。线程池使用面临的核心的问题在于:线程池的参数并不好配置 。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大错误的配置可能导致:

  • 资源浪费:线程数过多引发CPU上下文切换开销剧增;
  • 请求堆积:队列过长导致超时雪崩;
  • 服务降级:线程数不足引发请求拒绝RejectExecutionException

二、线程池参数公认

✅先说结论:线程池设置多大,并没有固定答案, 需要结合实际情况不断的测试才能得出最准确的数据。

1. 基础默认:Little Law

java 复制代码
线程数(N)= QPS x (任务平均执行时间 / 1000)
  • QPS:每秒请求量 (如:A接口每秒访问量为5000,那边A接口的QPS即为5000);
  • 任务平均执行时间 (Task Avg RT):单个任务从提交到完成的时间(单位:毫秒);

2. 考虑任务类型

  • CPU密集型(如计算哈希):线程数 ≈ CPU核心数 + 1;
  • IO密集型(如数据库查询):线程数 = CPU核心数 x (1 + 等待时间 / 计算时间);

举个例子:某订单处理任务平均执行时间为:50ms,其中30ms为数据库等待时间,那么合理的线程数为:

线程数 = 8核 x (1 + 30/20) = 20;

3. 业界的一些线程池参数配置方案

4. 实际生产案例:物联网充电桩C端扫码进入业务处理线程池调优

场景描述:

  • 目标QPS:8000
  • 任务类型:IO密集型(含数据库操作)
  • 单任务平均耗时:约80ms(含50ms数据库等待)

参考计算:

scss 复制代码
理论线程数 = 8000 × (80/1000) = 640;
实际线程数 = CPU核心数 × (1 + 等待时间 / 计算时间) = 32核 × (1 + 50/30) ≈ 85;

最终配置:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    85,     // 核心线程数
    150,    // 最大线程数(应对瞬时峰值)
    60,     // 空闲线程存活时间(秒)
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(2000),  // 队列容量
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

压测结果(注意:非百分百精确,不同机器结果不同):

配置版本对比 QPS 平均响应时间 错误率 CPU利用率
优化前 4200 120ms 15% 78%
优化后 8500 75ms 0.2% 56%

5. 结论

实际上并没有得出通用的线程池计算方式。并发任务的执行情况和任务类型`相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,但这种占比是较难合理预估的,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。

三、线程池核心设计与实现

线程池是一种通过"池化"思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类,想深入理解框架板块的知识点,必须是从框架的底层实现原理入手,层层深入,线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了"池化"(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

①ThreadPoolExecutor参数含义

coolPoolSize:线程池核心线程数大小

是线程池中的一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut,简单来说线程池分为两个部分,核心线程池和非核心线程池,核心线程池中的线程一旦创建便不会被销毁,非核心线程池中的线程在创建后如果长时间没有被使用则会被销毁。

maximunPoolSize:线程池最大线程数量

整个线程池的大小,此值大于等于1。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。工作队列满,且线程数等于最大线程数,此时再提交任务则会调用拒绝策略。maximumPoolSize - corePoolSize = 非核心线程池的大小。

keepAliveTime:多余的空闲线程存活时间

非核心线程池中的线程在keepAliveTime时间内没有被使用就会被销毁,时间单位由TimeUnit unit决定。

当线程空闲时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。

TimeUnit unit:空闲线程存活时间单位

keepAliveTime的计量单位

BlockingQueue workQueue:任务队列

阻塞队列用来存储任务,当有新的请求线程处理时,如果核心线程池已满,新来的任务会放入workQueue中,等待线程处理,JUC提供的阻塞队列有很多,例如:ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue等。

ThreadFactory:工厂类对象

线程池的创建传入了此参数,是通过工厂类中的newThread()方法来实现的。

RejectedExecutionHandler handler:拒绝策略

如果线程池中没有空闲线程,已存在maximumPoolSize个线程,且阻塞队列workQueue已满,这时再有新的任务请求线程池执行,会触发线程池的拒绝策略,可以通过参数handler来设置拒绝策略,注意只有有界队列例如:ArrayBlockingQueue或者指定大小的 LinkedBlockingQueue 等拒绝策略才有用,因为无解队列拒绝策略永远不会被触发。

②ThreadPoolExecutor运行流程

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

1️⃣ 直接申请线程执行该任务;

2️⃣ 缓冲到队列中等待线程执行;

3️⃣ 拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

③任务执行机制

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

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

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

④任务缓冲机制

任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列BlockingQueue是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:

使用不同的队列可以实现不一样的任务存取策略。在这里,我们可以再介绍下阻塞队列的成员:

⑤任务拒绝策略

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

csharp 复制代码
public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,其特点如下:

策略名称 描述
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常,这线程池默认的拒绝策略,抛出异常,并及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常,使用此策略,可能会使我们无法发现系统的异常状态,建议一些无关紧要的业务采用此策略。
ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新提交被拒绝的任务,是否要采用这种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
ThreadPoolExecutor.CallerRunsPolicy 由调度线程(提交任务的线程)处理该任务,这种情况是需要让所有的任务都执行完毕,这种策略适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。
csharp 复制代码
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        
        public CallerRunsPolicy() { }

        /**
         * 只要线程池没有被关闭,那么由提交任务的线程自己来执行这个任务
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

 public static class AbortPolicy implements RejectedExecutionHandler {
       
        public AbortPolicy() { }

        /**
         * 不管怎样,直接抛出RejectedExecutionException 异常,是线程池默认的策略,
         * 如果在自定义线程池的时候不传相应的handler的话,那么就会使用这个拒绝策略。
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
public static class DiscardPolicy implements RejectedExecutionHandler {
       
        public DiscardPolicy() { }

        /**
         * 不做任何处理,直接忽略掉这个任务
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            
        }
    }

   public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        
        public DiscardOldestPolicy() { }

        /**
         * 如果线程池没有被关闭的话,把队列头的任务(也就是等待了最长时间的)直接扔掉,然后提交这个任务到仍待队列中
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

⑥核心源码分析-ThreadPoolExecutor

ThreadPoolExecutor的execute方法

scss 复制代码
// 线程池状态和线程数整数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));


public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // 获取线程池状态和线程数
        int c = ctl.get();
        // 如果当前线程数小于核心线程数,创建Worker线程并启动线程
        if (workerCountOf(c) < corePoolSize) {
            // 添加任务成功,到这里就结束了,结果会包装到FutureTask中
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 要么当线程数大于等于核心线程数,要么刚刚addWorker失败了,如果线程池处理RUNNING状态,会把这个任务添加到任务队列中workQueue中
        if (isRunning(c) && workQueue.offer(command)) {
            // 二次状态检查
            int recheck = ctl.get();
            // 如果线程池已不处于RUNNING状态,那么移除已经入队列的这个任务,并且执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
                // 如果线程池还是RUNNING状态,并行线程数为0,重新创建一个新的线程,这个的目的是担心任务提交的到队列中了,但是线程都关闭了
            else if (workerCountOf(recheck) == 0)
                // 创建Worker,并启动里面的Thread,为什么传null,线程启动后会自动从阻塞队列拉取任务执行
                addWorker(null, false);
        }
        // 如果workQueue队列满了,那么进入到这个分支,以maximumPoolSize为界创建新的worker线程并启动,如果失败,说明当前线程数已经达到maximumPoolSize,并执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

ThreadPoolExecutor的Worker

来看下线程Worker作为线程池中真正执行任务的线程,继承了抽象类AbstractQueuedSynchronizer,用AQS来实现独占锁,为的就是实现不可重入的特性去反应线程现在的执行状态。

scala 复制代码
    private final class Worker extends AbstractQueuedSynchronizer implements Runnable {

        /** 真正的线程 */
        final Thread thread;
        /** 这里的Runnable是任务,这个线程起来逸煌需要执行的第一个任务,第一个任务就是存放在这里的(注意:线程可不止执行这一个任务) */
        Runnable firstTask;
        /** 用于存放此线程完成的任务数,注意,这里使用了volatile,保证可见性 */
        volatile long completedTasks;

        // TODO: switch to AbstractQueuedLongSynchronizer and move
        // completedTasks into the lock word.

        /**
         * Worker只有一个构造方法,传入firstTask也可以传如null
         */
        Worker(Runnable firstTask) {
            setState(-1); 
            this.firstTask = firstTask;
            // 调用 ThreadFactory来创建一个新的线程
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker. */
        public void run() {
            runWorker(this);
        }
    }

ThreadPoolExecutorexecute方法中创建Worker就是调用了下面的addWorker方法,该方法 2 个入参。第一个参数是准备提交给这个线程执行的任务,当为null时,线程启动后会自动从阻塞队列拉任务执行。

第二个参数为true代表使用核心线程数作为创建线程的界限,也就说创建这个线程的时候,如果线程池中的线程总数已经达到核心线程数,那么不能响应这次创建线程的请求 如果是false,代表使用最大线程数 作为界限同理。

ini 复制代码
// 这个是真正的线程
     final Thread thread;


      private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
                        
               // 当线程池处于 SHUTDOWN 的时候,不允许提交任务,但是已有的任务继续执行
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                  // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务了 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 重试 continue retry;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                  // 由于有并发,重新再读取一下 ctl
                c = ctl.get(); 
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
                
        // worker 是否已经启动
        boolean workerStarted = false; 
        // 是否已将这个 worker 添加到 workers 这个 HashSet 中
        boolean workerAdded = false; 
        Worker w = null;
        // 可以开始创建线程来执行任务了
        try { 
              // 把 firstTask 传给 worker 的构造方法
            w = new Worker(firstTask); 
              // 取 worker 中的线程对象,之前说了,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程
            final Thread t = w.thread; 
            if (t != null) {
                   // 整个线程池的全局锁
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
                                        // 小于 SHUTTDOWN 那就是 RUNNING
                    if (rs < SHUTDOWN || 
                        // 如果等于 SHUTDOWN,前面说了,不接受新的任务,但是会继续执行等待队列中的任务
                        (rs == SHUTDOWN && firstTask == null)) { 
                          // worker 里面的 thread 可不能是已经启动的
                        if (t.isAlive())  
                            throw new IllegalThreadStateException();
                        // 加到 workers 这个 HashSet 中
                        workers.add(w); 
                        int s = workers.size();
                        // largestPoolSize 用于记录 workers 中的个数的最大值
                        if (s > largestPoolSize) 
                            // 因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值
                            largestPoolSize = s; 
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                // 添加成功的话,启动这个线程
                if (workerAdded) { 
                    // 启动线程
                    t.start(); 
                    workerStarted = true;
                }
            }
        } finally {
            // 如果线程没有启动,需要做一些清理工作,如前面 workCount 加了 1,将其减掉
            if (! workerStarted) 
                addWorkerFailed(w);
        }
        // 返回线程是否启动成功
        return workerStarted; 
    }

Worker线程真正的执行逻辑为runWorker方法实现如下:

ini 复制代码
// 此方法由 worker 线程启动后调用,这里用一个 while 循环来不断地从等待队列中获取任务并执行  
final void runWorker(Worker w) { 
        Thread wt = Thread.currentThread();
              // worker 在初始化的时候,可以指定 firstTask,那么第一个任务也就可以不需要从队列中获取
        Runnable task = w.firstTask; 
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            // 循环调用 getTask 获取任务
            while (task != null || (task = getTask()) != null) { 
                w.lock();
                // 如果线程池状态大于等于 STOP,那么意味着该线程也要中断
                if ((runStateAtLeast(ctl.get(), STOP) || 
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    // 这是一个钩子方法,留给需要的子类实现
                    beforeExecute(wt, task); 
                    Throwable thrown = null;
                    try {
                           // 到这里终于可以执行任务了
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        // 这里不允许抛出 Throwable,所以转换为 Error
                        thrown = x; throw new Error(x); 
                    } finally {
                        // 钩子方法,将 task 和异常作为参数,留给需要的子类实现
                        afterExecute(task, thrown); 
                    }
                } finally {
                    // 置空 task,准备 getTask 获取下一个任务
                    task = null; 
                    // 累加完成的任务数
                    w.completedTasks++; 
                    // 释放掉 worker 的独占锁
                    w.unlock(); 
                }
            }
            completedAbruptly = false;
        } finally {
            // 获取不到任务时,主动回收自己 执行线程关闭 可能getTask 返回 null,也就是说,队列中已经没有任务需要执行了,执行关闭,或者任务执行过程中发生了异常.
            processWorkerExit(w, completedAbruptly); 
        }
    

简单来说Worker线程启动后调用,会通过while循环来不断地通过getTask方法从等待队列中获取任务并执行达到线程回收。

getTask的实现也比较简单,阻塞直到获取到任务返回,keepAliveTime超时退出。

ini 复制代码
private Runnable getTask() {
        boolean timedOut = false; 

        for (;;) {
            int c = ctl.get();
            // 获取线程池的状态
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                // CAS 操作,减少工作线程数
                decrementWorkerCount(); 
                return null;
            }
                        
              // 获取线程池中的线程数
            int wc = workerCountOf(c);

            // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try { 
                // 到 workQueue 中获取任务
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
               // 如果此 worker 发生了中断,采取的方案是重试
               // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量,那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null
                timedOut = false; 
            }
        }
    

💥 需要注意的是:

线程池处于 SHUTDOWN ,而且 workQueue是空的,该方法返回 null ,这种不再接受新的任务。

线程池中有大于 maximumPoolSize workers存在,这种可能是因为有可能开发者调用了 setMaximumPoolSize() 将线程池的 maximumPoolSize调小了,那么多余的 Worker就需要被关闭。

线程池处于 STOP ,不仅不接受新的线程,连 workQueue中的线程也不再执行。

如果此 worker发生了中断,采取的方案是重试,也就是说如果开发者将 maximumPoolSize调小了,导致其小于当前的 workers数量,那么意味着超出的部分线程要被关闭。重新进入 for循环获取任务。

java 复制代码
 public void setMaximumPoolSize(int maximumPoolSize) {
        if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
            throw new IllegalArgumentException();
        this.maximumPoolSize = maximumPoolSize;
        if (workerCountOf(ctl.get()) > maximumPoolSize)
            // 中断 worker 重试 超出的部分线程要被关闭
            interruptIdleWorkers();
    }

  private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

四、避坑指南:线程池配置的几大误区💥

简单列举一下我自己的日常开发中亲身踩过的一些坑,以及如何并没这些坑:

1. 直接使用Executors

许多初学者在创建线程池时,直接使用Executors提供的快捷方法:

ini 复制代码
ExecutorService executor = Executors.newFixedThreadPool(10);

Executors创建返回线程池的弊端:

  • FixedThreadPoolSingleThreadPool:允许的请求队列长度是Integer.MAX_VALUE可能会堆积大量的请求 ,导致OOM
  • CachedThreadPoolSchduledThreadPool:允许的场景线程数量为Integer.MAX_VALUE可能会创建大量的线程 ,导致OOM

直接使用问题在哪?

  • 无界队列:newFixedThreadPool使用的队列是LinkedBlockingQueue,它是无界队列,任务堆积可能会导致内存溢出。
  • 线程无限增长:newCachedThreadPool会无限创建线程,在任务量激增时可能耗尽系统资源。

示例:内存溢出的风险

ini 复制代码
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000000; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

💥当任务数远大于线程数的时候,导致任务无无限堆积在队列中,最终导致内存溢出OutOfMemoryError

解决办法

使用ThreadPoolExecutor,并明确指定参数:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,
    4,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100), // 有界队列
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

2. 错误配置导致的线程过载

一些初级开发者随意配置线程池参数,比如核心线程数 10,最大线程数 100,看起来没问题,但这可能导致性能问题或资源浪费,比如我实际看到过这样配置:

ini 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, // 核心线程数
    100, // 最大线程数
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10)
);

for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(5000); // 模拟耗时任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

❌这种配置带来的后果:在任务激增时,会创建大量线程,系统资源被耗尽。

正确配置方式

根据任务类型选择合理的线程数:

  • CPU密集型:线程数建议设置为CPU核心数 + 1。
  • IO密集型:线程数建议设置为2 * CPU核心数。

正确示例

ini 复制代码
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    cpuCores + 1,
    cpuCores + 1,
    60L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(50)
);

3. 忽略任务队列的选择

任务队列直接影响线程池的行为。如果选错队列类型,会带来很多隐患。

日常开发中常见队列的坑

  • 无界队列Boundless Queue:任务无限堆积;
  • 有界队列Bounded Queue:队列满了会触发拒绝策略;
  • 优先级队列Priority Queue:容易导致高优先级任务频繁抢占低优先级任务。

4. 忽略拒绝策略:拒绝策略设置错误导致接口超时

线程池拒绝策略可以说一个常见八股文问题。可能基本上都能记住了线程池有四种决绝策略,可是实际代码编写中,发现大多数人都只会用CallerRunsPolicy策略(由调用线程处理任务)。我吃过这个亏,因此也拿出来讲讲。

4.1. 问题原因

曾经有一个线上业务接口使用了线程池进行第三方接口调用,线程池配置里的拒绝策略采用的是CallerRunsPolicy。示例代码如下:

java 复制代码
// 某个线上线程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        20, // 最小核心线程数
        50, // 最大线程数,当队列满时,能创建的最大线程数
        60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
        new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
        new CustomizableThreadFactory("task"), // 自定义线程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
);

threadPoolExecutor.execute(() -> {
    // 调用第三方接口
    ...
});

在第三方接口异常的情况下,线程池任务调用第三方接口一直超时,导致核心线程数、最大线程数堆积被占满、阻塞队列也被占满的情况下,也就会执行拒绝策略,但是由于使用的是CallerRunsPolicy策略,导致线程任务直接由我们的业务线程来执行。

因为第三方接口异常,所以业务线程执行也会继继续超时,线上服务采用的Tomcat容器,最终也就导致Tomcat的最大线程数也被占满,进而无法继续向外提供服务。

4.2. 解决方法

首先我们要考虑业务接口的可用性,就算线程池任务被丢弃,也不应该影响业务接口。

在业务接口稳定性得到保证的情况下,在考虑到线程池任务的重要性,不是很重要的话,可以使用DiscardPolicy策略直接丢弃,要是很重要,可以考虑使用消息队列来替换线程池。

5. 阻塞任务占用线程池

如果线程池中的任务是阻塞的(如文件读写、网络请求),核心线程会被占满,影响性能。

比如:

scss 复制代码
executor.submit(() -> {
    Thread.sleep(10000); // 模拟阻塞任务
});

5.1. 改进方法

  • 减少任务的阻塞时间。
  • 增加核心线程数。
  • 使用异步非阻塞方式,如NIO

6. 未实现动态调整线程池参数

ini 复制代码
executor.setCorePoolSize(20);
executor.setMaximumPoolSize(50);

建议走配置动态配置线程池,比如通过项目本身yml/properties配置,或者借助配置中心配置,比如:NacosApollo,也可以集成第三方场插件,比如:DynamicTp

五、总结

实际上线程池的整体架构设计思想是值得我们深入研究和学习的,现如今很多架构上都借鉴了"池化"的设思想,比如:数据库连接池,Tomcat线程池,业务开发中的线程池,NettyEventLoopGroup,甚至于HTTP的长连接也是复用TCP连接,和池化思想很相似。好了,今天的分享就到此结束了,如果文章对你有所帮助,欢迎:点赞👍+评论💬+收藏❤ ,我是:IT_sunshine ,我们下期见!

相关推荐
Chase_Mos29 分钟前
Spring 必会之微服务篇(1)
java·spring·微服务
懵逼的小黑子2 小时前
Django 项目的 models 目录中,__init__.py 文件的作用
后端·python·django
小林学习编程3 小时前
SpringBoot校园失物招领信息平台
java·spring boot·后端
撸码到无法自拔3 小时前
docker常见命令
java·spring cloud·docker·容器·eureka
heart000_13 小时前
IDEA 插件推荐:提升编程效率
java·ide·intellij-idea
ŧ榕树先生4 小时前
查看jdk是否安装并且配置成功?(Android studio安装前的准备)
java·jdk
未来的JAVA高级开发工程师4 小时前
适配器模式
java
LUCIAZZZ4 小时前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot
D_aniel_4 小时前
排序算法-计数排序
java·排序算法·计数排序
极小狐4 小时前
极狐GitLab 通用软件包存储库功能介绍
java·数据库·c#·gitlab·maven