一句话说透Java里面的线程池原理

243 阅读3分钟

一句话总结:
Java 的线程池就像快递站的智能调度系统——固定员工 + 临时工 + 任务排队区 + 爆仓应急方案,保证包裹(任务)高效处理,不挤爆仓库(内存)!


一、线程池的四大核心组件

想象一个快递站的管理系统:

  1. 核心快递员(核心线程数)

    • 长期雇佣的正式员工,即使没包裹(任务)也留着,随时待命。
    • 默认不偷懒:除非设置 allowCoreThreadTimeOut(true),否则永不裁员。
  2. 临时快递员(最大线程数 - 核心线程数)

    • 忙不过来时临时招的兼职,闲太久(超过 keepAliveTime)就开除。
  3. 包裹暂存区(任务队列)

    • 核心快递员都在忙时,新包裹先放这里排队。

    • 常见队列类型

      • 无限仓库LinkedBlockingQueue):容易爆仓(内存溢出)!
      • 有限货架ArrayBlockingQueue):货架满了触发招临时工。
  4. 爆仓应急方案(拒绝策略)

    • 包裹太多,货架满 + 临时工招满 → 启动应急方案:

      • 拒收包裹AbortPolicy):直接抛异常,默认策略。
      • 让寄件人自己送CallerRunsPolicy):谁提交任务谁自己执行。
      • 丢弃最旧包裹DiscardOldestPolicy):把队首任务扔掉,塞新任务。
      • 默默丢弃DiscardPolicy):直接无视新任务。

二、线程池工作流程(包裹处理流水线)

  1. 来新包裹(提交任务)

    • 第1步:有空闲核心快递员 → 立刻处理。
    • 第2步:核心快递员全忙 → 包裹进暂存区排队。
    • 第3步:暂存区满 → 招临时快递员(直到达到最大线程数)。
    • 第4步:临时工也招满 → 触发爆仓应急方案!

流程图:

新任务 → 核心线程有空? → 是 → 立刻执行  
       ↓ 否  
       → 队列未满? → 是 → 入队等待  
       ↓ 否  
       → 还能招临时工? → 是 → 创建临时线程执行  
       ↓ 否  
       → 执行拒绝策略  

三、线程复用秘密:快递员的摸鱼循环

快递员(线程)怎么做到一直干活不辞职?

// 简化版线程运行逻辑  
while (任务 != null || 允许摸鱼) {  
    任务 = 队列.take(); // 从队列取任务(没任务就等)  
    执行任务;  
}  
// 如果允许摸鱼时间到,且是临时工 → 辞职走人  
  • 核心快递员:默认一直等任务(queue.take() 会阻塞)。
  • 临时快递员:用 queue.poll(keepAliveTime) 等任务,超时后自杀。

四、参数配置口诀:按需定制团队

  1. CPU 密集型任务(如计算圆周率):

    • 推荐:核心线程数 = CPU 核数,队列用有界队列。
    • 原因:线程太多会导致频繁切换,反而降低效率。
  2. IO 密集型任务(如网络请求):

    • 推荐:核心线程数 = 2 * CPU 核数,最大线程数适当调高。
    • 原因:线程等待 IO 时不占用 CPU,可并行更多任务。
  3. 混合型任务

    • 推荐:拆分为 CPU 密集和 IO 密集两部分,分别用不同线程池。

五、避坑指南:别让线程池坑了你!

  1. 慎用无界队列

    • 错误:new FixedThreadPool(5) → 默认用无界队列,任务堆积 → 内存溢出!
    • 正确:手动创建线程池,用有界队列(如 ArrayBlockingQueue)。
  2. 拒绝策略要合理

    • 错误:线上服务用 AbortPolicy → 任务提交失败直接抛异常,服务崩溃!
    • 正确:用 CallerRunsPolicy 或自定义策略(如记录日志 + 降级处理)。
  3. 监控线程池状态

    • 关键指标:活跃线程数、队列大小、完成任务数。
    • 工具:Spring Boot Actuator、自定义监控日志。

六、代码示例:手动创建安全线程池

ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    5,                              // 核心线程数(正式员工)  
    10,                             // 最大线程数(正式+临时)  
    60, TimeUnit.SECONDS,           // 临时工摸鱼60秒后开除  
    new ArrayBlockingQueue<>(100),   // 有界队列(容量100)  
    new ThreadFactory() {           // 给快递员起名字(方便排查问题)  
        private AtomicInteger count = new AtomicInteger(1);  
        public Thread newThread(Runnable r) {  
            return new Thread(r, "快递员-" + count.getAndIncrement());  
        }  
    },  
    new ThreadPoolExecutor.CallerRunsPolicy() // 爆仓了让提交任务的人自己干!  
);  

总结口诀:

“线程池,四件套,核心临时队拒绝。
任务提交先看核,核满入队再招人。
队列若满拒任务,参数按需别瞎配。
无界队列是炸弹,拒绝策略要谨慎!”