Android线程池深入理解

1 基本概念

  • 线程池,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。
  • 线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程,工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。
  • 线程池的概念类似于生活中的一些排队场景,比如在火车站排队购票、在医院排队挂号、在银行排队办理业务等,一般都由若干个窗口提供服务,这些服务窗口类似于工作者线程,而队列的概念是类似排队的队伍。

2 线程池的优点

  • 它可以重用线程,避免线程创建的开销。
  • 在任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。

3 理解线程池

3.1 构造方法

ThreadPoolExecutor有多个构造方法,都需要一些参数,主要构造方法有:

public ThreadPoolExecutor(
          int corePoolSize,
          int maximumPoolSize,
          long keepAliveTime,
          TimeUnit unit,
          BlockingQueue<Runnable> workQueue,
          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, 
keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}                    

3.2 线程池大小

线程池的大小主要与四个参数有关:

  • corePoolSize:表示线程池中的核心线程个数,但并不是一开始就创建这么多线程,刚创建一个线程池后,不会预先创建核心线程,只有当有任务时才会创建;而且核心线程不会因为空闲而被终止,keepAliveTime参数不适用于它

  • maximumPoolSize:表示线程池中的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值大的线程个数。

  • keepAliveTime:表示当线程池中的线程个数大于corePoolSize时,额外空闲线程的存活时间。也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。如果该值为0,表示所有线程都不会超时终止

  • unit:是keepAliveTime参数的时间单位,参数为TimeUnit的枚举,常见的有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECOND(秒) 等。

  • 一般情况下,有新任务到来的时候,如果当前线程个数小于corePoolSize,就会直接创建一个新线程来执行该任务,即使其他线程现在也是空闲的,也会创建新线程

  • 如果当前线程个数大于等于corePoolSize,那就不会立即创建新线程了,它会先尝试请求加入队列,它是"尝试"入队,而不是"阻塞等待"入队

  • 如果队列满了或其他原因不能立即入队,它就不会入队,而是检查线程个数是否达到了maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到maximumPoolSize。否则任务将被拒绝。

3.3 队列

ThreadPoolExecutor要求的队列类型是阻塞队列BlockingQueue,它们都可以用作线程池的队列,比如:

  • LinkedBlockingQueue:基于链表的阻塞队列,可以指定最大长度,但默认是无界的。如果用的是无界队列,创建的线程就不会超过 corePoolSize,到达corePoolSize后,新的任务总会排队,参数maximumPoolSize也就没有意义了。
  • ArrayBlockingQueue:基于数组的有界阻塞队列,有助于防止资源耗尽
  • PriorityBlockingQueue:基于堆的无界阻塞优先级队列。
  • SynchronousQueue:直接提交。没有实际存储空间的同步阻塞队列,当尝试排队时,只有正好有空闲线程在等待接受任务时,则其中一个空闲线程接受该任务;否则总是会创建新线程,直到达到maximumPoolSize。

3.4 任务拒绝策略

如果队列有界,且maximumPoolSize有限,则当队列排满,线程个数也达到了maximumPoolSize,这时新任务来了,如何处理呢?此时,会触发线程池的任务拒绝策略。需要强调下,拒绝策略只有在队列有界,且maximumPoolSize有限的情况下才会触发。
  默认情况下,提交任务的方法如execute/submit/invokeAll等会抛出异常,类型为RejectedExecutionException。不过,拒绝策略是可以自定义的,ThreadPoolExecutor实现了四种处理方式:

  • ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常。
  • ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛异常,也不执行。
  • ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后自己排队。
  • ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行。

4 线程池的类型及区别

类Executors提供了一些静态工厂方法,可以方便的创建一些预配置的线程池,主要方法有:

(1) newSingleThreadExecutor:只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此适用于需要确保所有任务被顺序执行的场合。

public static ExecutorService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1,
          0L, TimeUnit.MILLISECONDS,
          new LinkedBlockingQueue<Runnable>());
}

只使用一个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。

(2)newFixedThreadPool:线程固定,且不会被回收,能够更快的响应外界请求。比较适合在系统负载高下,通过队列对新任务排队,保证有足够的资源处理实际的任务

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
         nThreads, nThreads,
         0L, TimeUnit.MILLISECONDS,
         new LinkedBlockingQueue<Runnable>());
}

使用固定数目的n个线程,使用无界队列LinkedBlockingQueue,线程创建后不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗非常大的内存。

(3)newCachedThreadPool:核心线程为0,非核心线程数量相当于无限大,任何任务都会被立即执行。比较适合在系统负载不太高下,执行大量的执行时间比较短的任务

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
          0, Integer.MAX_VALUE,
          60L, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>());
}

它的corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,keepAliveTime是60秒,队列为SynchronousQueue。含义是,当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制。对任一空闲线程,如果60秒内没有新任务,就终止。

(4) ScheduledThreadPool:核心线程数量固定,非核心线程数量不定的线程池。适合执行定时任务或者具有周期性的重复任务

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, 
          MILLISECONDS,
          new DelayedWorkQueue());
}

ScheduledThreadPool的核心线程数量是固定的,由传入的corePoolSize参数决定,非核心线程数量可以无限大。非核心线程闲置回收的超时时间为10秒( DEFAULT_KEEPALIVE_MILLIS的值为10L)。

5 阿里Android手册的强制要求

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

  • FixedThreadPool和SingleThreadPool : 允 许 的 请 求 队 列 长 度 为
    Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
  • CachedThreadPool和ScheduledThreadPool : 允 许 的 创 建 线 程 数 量 为
    Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
//正例
//返回可用处理器的Java虚拟机的数量
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 cachedThreadPool = 
Executors.newCachedThreadPool();

6 总结

ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非重新发明轮子,自己管理和维护消费者线程及任务队列。

7 参考链接

计算机程序的思维逻辑 (78) - 线程池

Android 线程池的类型、区别以及为何要用线程池

阿里Anddroid开发手册

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

推荐阅读更多精彩内容

  • 为什么使用线程池 当我们在使用线程时,如果每次需要一个线程时都去创建一个线程,这样实现起来很简单,但是会有一个问题...
    闽越布衣阅读 4,276评论 10 45
  • 深入分析线程池 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如...
    史路比阅读 447评论 0 1
  • 1 小A跟我说起一件事。 入职两年多荣升人事主管,简直是一部杜拉拉现实励志剧,小A真的很优秀,身边人也都这么夸。 ...
    妙语一百阅读 349评论 0 1
  • 有点急躁,感受不到像以前中学时期的进步。 现在,好好写,不退步都是好的了。以前爱尝试,人成熟了,创造力却没有了。 ...
    顾珩久阅读 172评论 0 0
  • 1对己真诚 网上有个段子说,你最不该相信的人就是你自己,因为这个人已经骗了你几十年了。 我想很多人都会有说到做不到...
    幽游呦佑阅读 641评论 0 0