美文网首页
出事以后才知道线程池参数不能随便设置,长记性了

出事以后才知道线程池参数不能随便设置,长记性了

作者: 码农翻身记 | 来源:发表于2020-01-22 17:11 被阅读0次

    一、前言

    相信作为安卓开发者来说,线程池应该不陌生,4种类型线程池也能随口说出,但是由于对线程池的参数理解不够深入,前阵子在项目中应用线程池,因为参数设置不好,带来了一些场景下的崩溃。

    二、事件回顾

    前阵子上线了全新的性能监控平台,线上APP的性能问题一下子暴露出来了,这些问题在开发和测试阶段,还有第三方几百款手机的适配测试都没测出来。
    有个地方是处理用户数据,涉及在内存中对用户消息数据读写删等操作,并涉及DB操作,以前考虑到用户的数据量不会很大,都是在主线程操作,一直以来也没收到任何的反馈,旧的日志系统也没出现过相关的崩溃记录,不过旧版系统没有ANR日志。
    新版性能监控平台,因为毕竟难收集客户的ANR日志trace文件,所以监控帧率回调,超过5s没响应则判断为ANR。上线后发现消息模块的ANR记录最多,累计起来占了90%以上,看到这个数字当即傻眼了。后来还查了有客户黑屏和闪退,都是这个模块引起。

    5s数据都没处理完,逻辑设计上肯定有问题,但是为了先把ANR和有关的黑屏问题解决,决定要改为在子线程处理。立即创建个线程池,所有涉及数据改动的方法都改为在子线程处理,自己模拟了大量的数据,完全没有问题,测试也没问题,后来灰度上线,第二天一早我打开性能平台看数据,相关的ANR都没了,暗高兴一把,可是再看崩溃日志,当即脑袋空白了,创建的线程池有抛\color{red}{RejectedExecutionException}异常的记录。。。

    三、出问题的参数设置

    看到崩溃日志后,立马意识到自己设置的线程池参数有问题,代码如下:

      mThreadPool = new ThreadPoolExecutor(0, 6, 60, TimeUnit.SECONDS,
                        new SynchronousQueue<Runnable>(), threadFactory("xxx", false));
    

    以前看线程池的说明也只关注前面4个参数,队列和异常都是大略看过,以为一般情况都无关紧要,这次定义线程池,还特意看了下okhttp任务分发类Dispatcher的线程池设置:

     public synchronized ExecutorService executorService() {
        if (executorService == null) {
          executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
              new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
        }
        return executorService;
      }
    

    然后认为项目中场景没必要把maximumPoolSize设置为Integer.MAX_VALUE,设置为6足够了,结果导致了悲剧,特定用户特定场景6个线程很快占用完。
    再重新查线程池三种队列的说明,以及拒绝策略,才知道使用SynchronousQueue是无缓存队列,6个线程池占满后,新的任务会被拒绝,然后拒绝策略使用了默认的策略,即崩溃日志中看到的RejectedExecutionException。

    四、线程池参数说明

    先看下线程池的构造方法:

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }
    
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 threadFactory, defaultHandler);
        }
    
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  RejectedExecutionHandler handler) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), handler);
        }
    
        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    

    各个参数的说明:
    1、corePoolSize:核心线程数
    核心线程一但创建就会一直存活,及时没有任务要执行,另外,如果当前核心线程数<corePoolSize,有新任务会优先创建新线程,即使当前有线程空闲,除非设置允许超时(allowCoreThreadTimeOut,默认false),超时会关闭。

    2、maximumPoolSize:最大线程数
    当线程数>=corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务。
    当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。

    3、keepAliveTime:线程空闲时间
    当当前线程数>corePoolSize后,空闲时间超过keepAliveTime的线程,会自动退出,直到当前线程数=corePoolSize,如果allowCoreThreadTimeOut设置为true,则直到当前线程数=0.

    4、unit:keepAliveTime的时间单位
    keepAliveTime的时间单位,枚举类型,有纳秒(NANOSECONDS)、微秒(MICROSECONDS)、毫秒(MILLISECONDS)、秒(SECONDS)、分(MINUTES)、时(HOURS)、天(DAYS)

    5、workQueue:任务缓存队列
    当前线程数>corePoolSize后,新任务会存入workQueue等待执行,workQueue是BlockingQueue实例,是一个先进先出的队列。

    6、threadFactory:创建线程的工厂类
    通常会自定义线程的名称,这样能快速识别线程是哪个工厂类创建的,方便快速跟踪问题。

    7、handler :任务拒绝策略
    当前线程数=maximumPoolSize,且workQueue已满,如果再有新任务进来,则会触发拒绝策略,拒绝新的任务。

    系统默认的策略有以下几种:

    • AbortPolicy:直接抛异常处理,为线程池默认的拒绝策略
    • DiscardPolicy:直接抛弃不处理
    • DiscardOldestPolicy:丢弃队列中最早的任务
    • CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理
      在最后一个构造方法中,可以传入自定义的拒绝策略,只要实现RejectedExecutionHandler接口,可以把任务保存到本地,也可以记录日志,人工处理或者后续优化参数。

    五、推荐的参数设置

    5.1、推荐的线程池参数设置

    这里引用阿里的Android开发规范:

    int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
    int KEEP_ALIVE_TIME = 1;
    TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
    
    ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES,
    NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, taskQueue,
    new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());
     //执行任务
     executorService.execute(new Runnnable() {
           ...
     });
    

    核心线程数:CPU可用核心数
    最大线程数:CPU可用核心数x2
    最大空闲时间:1s
    任务队列:任务等待队列,最大等待数量Integer.MAX_VALUE,超过后抛异常
    拒绝策略:自定义策略

    工作线程是不是越多越好?显然不是,CPU的核心数有限,所以并发处理的线程数也是有限的,1核CPU设置1000个线程数没有任何意义。
    对于核心线程数,一般情况参考阿里规范设置即可,但有更严谨的应用方案,区分IO密集型还是计算密集型:

    • IO密集型=2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络*
      数据传输等等)
    • 计算密集型=Ncpu(常出现于线程中:复杂算法)
      java中:Ncpu=Runtime.getRuntime().availableProcessors()

    5.2、一些建议(阿里Android规范)

    1、不显式创建线程
    反例:

    new Thread(new Runnable() {
          @Override
          public void run() {
                  //操作语句
                  ...
           }
    }).start();
    

    新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor或者其他形式自定义的线程池),或者使用HandlerThread,不允许自行显式创建线程,如上面反例。
    好处:
    使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 另外创建匿名线程不便于后续的资源使用分析,
    对性能分析等会造成困扰。

    2、不允许使用 Executors 去创建
    线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:
    Executors 返回的线程池对象的弊端如下:

    1. FixedThreadPool 和 SingleThreadPool : 允 许 的 请 求 队 列 长 度 为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
    2. CachedThreadPool 和 ScheduledThreadPool : 允 许 的 创 建 线 程 数 量 为Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

    3、定义线程名称
    新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查。

    public class MyThread extends Thread {
         public MyThread(){
              super.setName("ThreadName");
              …
          }
    }
    

    4、设置存活时间
    ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放。

    参考

    根据CPU核心数确定线程池并发线程数
    阿里巴巴Android开发手册

    相关文章

      网友评论

          本文标题:出事以后才知道线程池参数不能随便设置,长记性了

          本文链接:https://www.haomeiwen.com/subject/tyouzctx.html