事故总结集锦-主子线程池共用一个线程池导致的事故8(一周一更)

1,283 阅读3分钟

【问题描述】

线上商品图片个别为空白图片。

【影响范围】

APP首页、门店列表页、单品页详情

【事故级别】

P0

【处理过程】

11:23 反馈app商品图片很多都为空

11:30 定位后台接口缓存数据返回为空;

11:35 接口打开降级查询数据库开关,补全商品图片至缓存

11:36 问题解决

12:30 review代码,发现问题所在

【故障原因】

  • 商品数据由于图片调整大小类型进行数据重刷,在刷新的过程中为了缩短时间开启了多线程并行处理,在处理的过程中,按照商家下门店数开启了线程,并且门店下的sku图片刷新又按照 批量50个开启了子线程处理。
  • 主线程、子线程均来源于同一个线程池,并没有进行线程池隔离。
  • 主、子线程占用会导致相互等待。
  • 随着任务的增加,新生成的子线程,逐渐达到最大线程数,从而进入到等待队列
  • 等待队列对着任务的积压逐渐达到最大值
  • 走线程策略,此处使用的是拒绝策略,即任务丢失
  • 图片刷新的任务开始丢失,导致线上缓存数据缺失

【总结】

先来回顾下线程池的基础知识。

线程池重要参数:

  1. corePoolSize核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
  2. maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
  3. keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
  4. unit 时间单位
  5. workQueue 保存任务的阻塞队列
  6. threadFactory 创建线程的工厂
  7. handler 拒绝策略

任务执行顺序:

  1. 当线程数小于corePoolSize时,创建线程执行任务。
  2. 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
  3. 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
  4. 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。

ThreadPoolExecutor默认有四个拒绝策略:

  1. ThreadPoolExecutor.AbortPolicy() 直接抛出异常RejectedExecutionException
  2. ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行
  3. ThreadPoolExecutor.DiscardPolicy() 直接丢弃后来的任务
  4. ThreadPoolExecutor.DiscardOldestPolicy() 丢弃在队列中队首的任务

接下来我们通过一个demo来复现事故的原因过程

public class FuatureTaskDemo2 {
    private static ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(
            4,
            4,
            10L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(2),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardPolicy());

    /**
     * @return
     */
    public void getWorker(String name) throws Exception {
        System.out.println("执行"+name+"程任务开始");
        for(int i=0;i<10;i++){
            int finalI = i;
            mExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("执行"+name+"中子线程:"+ finalI);
                }
            });
        }
        int getActiveCount = (mExecutor).getActiveCount();
        int getCorePoolSize = (mExecutor).getCorePoolSize();
        int getMaximumPoolSize = (mExecutor).getMaximumPoolSize();
        long getTaskCount = (mExecutor).getTaskCount();
        BlockingQueue<Runnable> blockingQueue = (mExecutor).getQueue();
        System.out.println("getActiveCount"+getActiveCount);
        System.out.println("getCorePoolSize"+getCorePoolSize);
        System.out.println("getMaximumPoolSize"+getMaximumPoolSize);
        System.out.println("getTaskCount"+getTaskCount);
        System.out.println("blockingQueue"+blockingQueue.size());
        mExecutor.shutdown();
    }


    public static void main(String[] args) {
        FuatureTaskDemo2 it = new FuatureTaskDemo2();
        FuatureTaskDemo2 it3 = new FuatureTaskDemo2();
        try {
            it3.getWorker("父线程");
            it.getWorker("子线程");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

执行结果:

image.png

给我们的启示:

  • 由上述demo可以看到。30个子线程任务 最后只执行了 6个,其余的全部被拒绝。
  • 我们在执行核心数据线程的时候,尽量做到主-子线程池分离
  • 核心任务 拒绝策略一定是ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行,或者是 ThreadPoolExecutor.AbortPolicy() 直接抛出异常后进行重试。