ThreadPoolExecutor之市场雇工的故事

很乐意为你深入浅出地剖析Java四大线程池的设计思想。我们将通过一个"包工头与雇工"的比喻,结合代码和时序图,彻底讲明白它们的原理。


核心思想:为什么需要线程池?

想象一个场景:

你是一个包工头(任务提交者 ),有很多零碎的任务(Runnable/Callable任务 )需要雇工(线程)来完成。比如搬砖、和水泥、砌墙。

  • 糟糕的做法 :每次来一个新任务,你就现去劳务市场招聘一个工人(new Thread()),任务干完就把他辞退(线程销毁)。这非常浪费时间和金钱(系统资源 ),招聘和培训的成本(创建和销毁线程的开销)可能比任务本身还高。
  • 聪明的做法 :你长期雇佣(线程复用 )一支稳定的工人队伍(线程池 )。有活时,派队伍里的工人去干;没活时,让工人们休息(阻塞等待)但不解散。这样效率极高,管理起来也方便。

Java的 ThreadPoolExecutor 就是帮你管理这支"队伍"的智能管理系统。


线程池的核心构造参数

在认识四大线程池前,必须先了解构建线程池的7个核心参数,它们决定了线程池的行为策略:

  1. corePoolSize(核心线程数) :长期雇佣的正式工人数,即使他们闲着也不开除。
  2. maximumPoolSize(最大线程数) :你的队伍最大规模,包括正式工和临时工。
  3. keepAliveTime(空闲线程存活时间) :临时工(超出核心线程数的线程)如果空闲这么久,就会被辞退以节省开支。
  4. unit(时间单位) :存活时间的单位。
  5. workQueue(工作队列) :一个任务队列。新任务来了,如果正式工都没空,就把任务先放在这个队列里排队。
  6. threadFactory(线程工厂) :用工标准,如何招聘和培训工人(如何创建新线程)。
  7. handler(拒绝策略) :当任务队列也满了,并且所有工人(线程)都在忙时,如何拒绝新来的任务。

四大线程池的本质,就是使用 Executors 工厂类,用不同的参数预先配置好了这个"智能管理系统"。


四大线程池详解

1. FixedThreadPool(固定大小线程池)

  • 雇工故事 :你是一个小作坊老板,你只长期雇佣了3个固定工人(corePoolSize = maximumPoolSize = 3)。任务来了就派给空闲的工人。如果3个工人都在忙,新任务就在工作台(无界队列 LinkedBlockingQueue)上排队等着。因为你地方小,也养不起更多工人,所以从不招聘临时工。队列可以无限长,理论上不会触发拒绝策略。

  • 设计思想:控制最大并发数,超出的任务排队执行。适用于负载较重的服务器,需要限制当前线程数量。

  • 代码创建

    java 复制代码
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
  • 源码参数

    java 复制代码
    new ThreadPoolExecutor(
        nThreads, // corePoolSize
        nThreads, // maximumPoolSize
        0L, TimeUnit.MILLISECONDS, // keepAliveTime为0,但因为core==max,所以此参数无效
        new LinkedBlockingQueue<Runnable>() // 无界队列
    );

2. CachedThreadPool(可缓存线程池)

  • 雇工故事 :你是一个劳务中介。一开始没有正式工(corePoolSize = 0)。有任务来了,就先看有没有空闲的临时工(maximumPoolSize = Integer.MAX_VALUE),如果有就派活;如果没有就立刻招聘一个新的临时工(创建新线程 )。临时工如果1分钟(keepAliveTime = 60s)没活干就被辞退。因为招聘速度极快(无核心线程,直接创建新线程 ),所以通常不需要任务排队(使用的队列是 SynchronousQueue,它不存储元素,只是做直接交接)。

  • 设计思想:无限扩展,随需随建,空闲回收。适用于执行很多短期异步任务的小程序,或负载较轻的服务器。

  • 代码创建

    java 复制代码
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
  • 源码参数

    java 复制代码
    new ThreadPoolExecutor(
        0, // corePoolSize
        Integer.MAX_VALUE, // maximumPoolSize,非常大(约21亿)
        60L, TimeUnit.SECONDS, // keepAliveTime
        new SynchronousQueue<Runnable>() // 同步移交队列
    );

3. SingleThreadExecutor(单线程线程池)

  • 雇工故事 :你是一个老板,但只雇佣了1个工人(corePoolSize = maximumPoolSize = 1)。所有任务都必须由这个工人按顺序完成。任务来了,他如果空闲就立马干,如果正在忙,新任务就在他旁边的工作队列(无界队列 LinkedBlockingQueue)里排好队,等他干完一件再干下一件。保证所有任务按提交顺序执行。

  • 设计思想:保证所有任务按顺序(FIFO, LIFO, 优先级)执行。不需要处理线程同步问题。

  • 代码创建

    java 复制代码
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  • 源码参数

    java 复制代码
    new ThreadPoolExecutor(
        1, // corePoolSize
        1, // maximumPoolSize
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>() // 无界队列
    );

4. ScheduledThreadPool(定时任务线程池)

  • 雇工故事 :你是一个物业经理,手下有一支专门负责定时任务的工人队伍(核心线程数 corePoolSize 由你指定)。他们的工作不是立即执行,而是:a) 在指定的延迟后执行一次(schedule);b) 定期固定延迟重复执行(scheduleWithFixedDelay);c) 定期固定频率执行(scheduleAtFixedRate)。它使用了一个特殊的延迟队列(DelayedWorkQueue)来安排任务执行的顺序和时间。

  • 设计思想:专门用于在给定的延迟后运行任务,或者定期执行任务。

  • 代码创建

    java 复制代码
    // 指定核心线程数
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
  • 核心用法

    java 复制代码
    // 延迟一次执行
    scheduledThreadPool.schedule(new RunnableTask(), 5, TimeUnit.SECONDS);
    
    // 固定频率执行(每次执行开始时间间隔固定)
    scheduledThreadPool.scheduleAtFixedRate(new RunnableTask(), 1, 3, TimeUnit.SECONDS);
    
    // 固定延迟执行(上次执行结束到下次执行开始间隔固定)
    scheduledThreadPool.scheduleWithFixedDelay(new RunnableTask(), 1, 2, TimeUnit.SECONDS);

调用过程时序图(以FixedThreadPool为例)

下图描绘了向一个 corePoolSize=2, maxPoolSize=3, queueSize=2 的线程池提交任务的完整流程,包括任务队列满时创建新线程(临时工),以及达到最大线程数后的拒绝策略。

流程文字解释:

  1. T1, T2 提交:核心线程数未满,直接创建核心Worker1和Worker2来执行。
  2. T3, T4 提交:核心线程已满,任务被放入工作队列排队。
  3. T5 提交 :核心线程忙,且队列已满(本例中队列大小为2),但当前总线程数(2)小于最大线程数(3),因此创建临时线程Worker3来执行T5。
  4. T6 提交 :核心线程忙 + 队列已满 + 线程数已达最大值 → 触发拒绝策略
  5. Worker事件循环 :每个Worker线程在完成手头任务后,会不断地去队列里取新的任务来执行。核心线程 会一直等待(take()),而非核心线程 会在超时时间内(poll(timeout))还拿不到任务时就被回收。

Android中的注意事项与实践

虽然 Executors 很方便,但在Android(尤其是App)开发中,需要谨慎使用:

  1. FixedThreadPool 和 SingleThreadExecutor :因为它们使用无界队列 LinkedBlockingQueue,如果任务积压过多,可能会导致内存溢出(OOM)。
  2. CachedThreadPool:因为它最大线程数几乎是无限的,如果任务数量非常多,可能会创建大量线程,耗尽系统资源,导致OOM或卡顿。

最佳实践:直接使用 ThreadPoolExecutor 自定义线程池。

根据你的业务场景(CPU密集型?IO密集型?),精心设置核心参数:

  • CPU密集型(如图像处理):核心线程数 ≈ CPU核数。
  • IO密集型(如网络请求):核心线程数可以设置得多一些,如 CPU核数 * 2。
  • 使用有界队列 (如 ArrayBlockingQueue)并指定合理的容量。
  • 定义明确的拒绝策略(如丢弃、回退到调用线程执行等)。
java 复制代码
// Android中推荐的自定义线程池示例
int corePoolSize = Runtime.getRuntime().availableProcessors();
int maxPoolSize = corePoolSize * 2;
long keepAliveTime = 60L;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(128);
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

ExecutorService myExecutor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    workQueue,
    threadFactory,
    handler
);

希望这个从架构师视角,结合故事、代码和时序图的讲解,能让你对Java四大线程池有彻底的理解。

相关推荐
2501_915909062 小时前
iOS 抓包工具有哪些?实战对比、场景分工与开发者排查流程
android·开发语言·ios·小程序·uni-app·php·iphone
锋风3 小时前
基于Binder的4种RPC调用
android
行墨4 小时前
CoordinatorLayout基本使用与分析—— Group 批量控制
android
行墨4 小时前
CoordinatorLayout基本使用与分析——水平偏移(Horizontal Bias)
android
私房菜5 小时前
Android dmabuf_dump 命令详解
android·libdmabufinfo·linmeminfo·dmabuf_dump
爱学啊5 小时前
1.Android Compose 基础系列:您的第一个 Kotlin 程序
android·kotlin·jetpack
maki0777 小时前
虚幻版Pico大空间VR入门教程 01 ——UE5 Android打包环境4.26~5.6
android·ue5·vr·虚幻·pico·大空间
行墨7 小时前
CoordinatorLayout基本使用与分析<五>
android
行墨7 小时前
CoordinatorLayout基本使用与分析<四>
android
行墨8 小时前
CoordinatorLayout基本使用与分析<三>
android