访问量 ...
访客数 ...
总文章数 186 篇
博客已运行 1977 天

多线程的智能调度专家

2025.03.03

智能调度五步法

在一个忙碌的餐厅后厨,线程池就像经验丰富的主厨团队,高效地处理各种任务请求。下面我们将通过一个餐厅的例子来解释线程池的工作原理。

餐厅后厨与线程池的类比:

  • 固定厨师(核心线程):5位常驻大厨随时待命
  • 临时帮厨(最大线程):高峰时段最多可调用10位帮手
  • 候菜区(任务队列):最多容纳100道待处理菜品
  • 客满策略(拒绝机制):超出接待能力时婉拒新客

当创建了线程池后,它会等待提交过来的任务请求。每当调用execute方法添加一个新任务时,线程池会按照以下步骤进行判断和处理:

程序世界的智能调度专家

  1. 新订单优先派给固定厨师。如果当前运行的线程数小于corePoolSize(即5位固定厨师),那么立即创建一个新的线程来运行该任务。
    // 当新任务到来时
    if (当前厨师 < 5) {
        立即安排空闲厨师处理;
    }
    
  2. 候菜区缓冲订单。如果当前运行的线程数已经达到或超过corePoolSize,但任务队列还未满,那么该任务会被放入任务队列中等待处理。
    // 所有厨师都在忙
    将新订单放入候菜区队列;
    等待厨师处理完当前菜品;
    
  3. 高峰时段调用临时帮厨。如果线程池任务队列满了且正在运行的线程数还小于maximumPoolSize(即10位帮厨),那么会创建非核心线程立刻运行这个任务,适应高峰期的需求。
    if (候菜区已满 && 当前厨师 < 10) {
        临时雇佣帮厨处理积压订单;
    }
    
  4. 客满时的智慧应对。如果任务队列满了且正在运行的线程数等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。具体处理方案如下:
    情况描述 处理方案 现实类比
    候菜区与帮厨全满 拒绝新订单 餐厅挂出"客满"牌
    可接受轻微延迟 由下单者自己处理 顾客自助打包带走
    优先保证最新订单 替换最久未处理订单 优先制作现点菜品
  5. 闲时精简团队。随着时间的推移,业务量越来越少,线程池中出现了空闲线程。当一个线程无事可做超过一定的时间时,线程池会进行判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
    // 营业低谷时段
    while (空闲厨师 > 5) {
        解散最近未工作的临时帮厨;
    }
    

为什么需要这位智能专家

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务。如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。 线程池是一种用于管理和复用线程的机制,可以有效地提高应用程序的性能和资源利用率。它的主要特点为:线程复用,提高响应速度,管理线程。

优势维度 传统模式痛点 线程池解决方案
资源消耗 频繁创建销毁线程开销大 重复利用现有线程
响应速度 新任务等待线程创建 立即分配空闲线程
系统稳定性 无限制创建导致资源耗尽 智能流量控制

默认线程池的致命陷阱

在实际开发中用哪个线程池?Executors类中已经给你提供了,为什么不用?

《阿里巴巴开发手册》明令禁止使用Executors工具类创建线程池,原因如下:

程序世界的智能调度专家

摘自《阿里巴巴开发手册》 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPoolSingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

生产事故真实案例:

  • 某金融系统崩溃事件:使用CachedThreadPool处理突发交易,瞬间创建3万个线程导致内存溢出。
  • 电商订单积压事故:FixedThreadPool无界队列堆积500万任务,占用80GB内存。
  • 物联网设备失联故障:SingleThreadPool阻塞导致10万台设备心跳超时。

自定义线程池的正确姿势

在实际开发中,使用自定义线程池是管理并发任务的一种有效方式。相比于使用Executors工厂类提供的预定义线程池,自定义线程池能够提供更灵活的配置选项。 在许多高并发场景下,推荐使用自定义线程池来精确控制资源分配和任务处理。

ThreadPoolExecutor类提供了七个核心参数来配置和管理线程池的行为:

参数名称 定义 推荐值 配置依据
corePoolSize 核心线程数 CPU核数 + 1 充分利用CPU资源
maximumPoolSize 最大线程数 CPU核数 * 2 应对突发流量
keepAliveTime 空闲线程存活时间 30-60秒 平衡资源释放速度
unit 时间单位 TimeUnit.SECONDS 定义keepAliveTime的具体时间单位
workQueue 任务队列 ArrayBlockingQueue(1000) 防止无限制堆积
threadFactory 线程工厂 Executors.defaultThreadFactory() 提供自定义的线程创建方式
rejectedHandler 拒绝策略 CallerRunsPolicy 保证核心业务不中断
public class MainTest {

   public static void main(String[] args) {
      // 定义线程池参数
      int corePoolSize = 2;  // 核心线程数
      int maxPoolSize = 5;   // 最大线程数
      long keepAliveTime = 1L;  // 空闲线程存活时间
      TimeUnit timeUnit = TimeUnit.SECONDS;  // 时间单位
      BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(3);  // 任务队列
      ThreadFactory threadFactory = Executors.defaultThreadFactory();  // 线程工厂
      RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();  // 拒绝策略

      // 创建自定义线程池
      ExecutorService executor = new ThreadPoolExecutor(
              corePoolSize,
              maxPoolSize,
              keepAliveTime,
              timeUnit,
              workQueue,
              threadFactory,
              handler);

      try {
         // 提交多个任务到线程池
         for (int i = 1; i <= 20; i++) {
            final int taskId = i;
            executor.execute(() -> {
               System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
            });
         }
      } catch (Exception e) {
         System.err.println("任务提交失败: " + e.getMessage());
      } finally {
         shutdownExecutor(executor);
      }
   }

}