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

一、前言

相信作为安卓开发者来说,线程池应该不陌生,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开发手册

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 什么是线程池?为什么要用线程池?Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都...
    stevefat阅读 471评论 0 0
  • 最先学习线程的时候,我有些一带而过,代码倒是看懂了,但是问题也有不少,导致对很多细节不清楚,所以在这多做一些笔记,...
    文茶君阅读 650评论 0 0
  • 一、为什么要用线程池? Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使...
    逆风fei扬阅读 231评论 1 0
  • 线程池 平时有接触过多线程开发的小伙伴们应该都或多或少都有了解、使用过线程池,而《阿里巴巴 Java 手册》里也有...
    端碗吹水阅读 1,996评论 0 7
  • 定义:在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程就工作线程 作用:1降低资源消耗:通过...
    小小少年Boy阅读 3,565评论 0 1