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四大线程池有彻底的理解。

相关推荐
用户2018792831672 小时前
AMS和app通信的小秘密
android
诺诺Okami2 小时前
Android Framework-Launcher-InvariantDeviceProfile
android
Antonio9153 小时前
【音视频】Android NDK 与.so库适配
android·音视频
sun00770012 小时前
android ndk编译valgrind
android
AI视觉网奇13 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空13 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet14 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin14 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo0305198716 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin