Thread
在Android应用开发过程中,我们不可避免的要使用网络请求去获取数据,或者从数据库、文件中读取数据,亦或需在业务开发中进行某些耗时操作。此时,就需要将这些任务放置于子线程中进行,使用线程的方式有很多种,除了线程池(Executor)之外,你可以使用AsyncTask、HandlerThread、IntentService等,你也可以直接实例化Thread调用其start来开启一个子线程。当然殊途同归,最终还是由Thread来但此重任。
可以直接在主线中操作么?
答案是肯定的,你也可以选择在主线中进行上述的除了“网络请求”之外的耗时操作,就跟自个儿烧菜吃一个儿道理,你可以清蒸、红烧、煲汤、爆炒着吃,也可以百无顾虑的选择生吃,当然吃坏肚子(ANR)就是另一回事了。那为什么我们说“除了网络请求之外”呢?难道网络请求就一定不能在主线程中进行了嘛?都是一个妈生的,凭啥我就不能在主线程里进行了,莫不是我长得特别丑,长得丑我就不能在自个儿的地盘上网撩妹了?
其实,在Android4.0(API14)之前,系统并没有限制你在主线程中进行网络请求,也就是说那个年代我们还能愉快的”上网撩妹“,当然结果自然是撩一次被扇一次脸。
由于网络请求必然是个耗时操作,更何况在移动网络并没有像有线宽带那么稳定的情况下(什么,你敢跟我说有线宽带稳定?打PC游戏的小伙伴们心里有句MMP不知道当不当讲)。
回归正题,我们从线程池的使用、各个参数意义、线程池的原理来逐步对其进行介绍。
一、线程池的基本使用及参数
之所以使用线程池,是为了尽可能减少资源的消耗,提高资源复用。“池”之所以为“池”,就是可以往里放东西,也可以从里取东西,而这个东西就指的是“线程”。我们不可能为每个任务的启动都去开一个线程,因为开辟一个线程也是需要消耗系统资源,不要以为开启子线程就不会造成界面卡顿了,线程开辟的过多意味着占用系统资源越多,这也是常见的卡顿原因。因此,我们需要线程池来充当对线程的管理角色,用于减少不必要线程的创建。
那么,线程池是怎么来管理如此多的子线程的呢?
首先,我们来看看线程池的类继承结构:
1. Executor定义了execute(Runnable command)接口,用于提交一个任务,该任务可以在新线程/线程池/任务提交线程执行
2. ExecutorService定义了submit、shutdown等多个接口,用于控制线程的提交、结束、进度的跟踪
3. AbstractExecutorService实现了ExecutorService接口,使用Future、Callable进行对线程的具体控制与结果回调,但他只是一个抽象类
4. ThreadPoolExecutor继承自AbstractExecutorService,也即我们真正接触的线程池类,可以看看源码注释:
译文大概是:ThreadPoolExecutor是一个用于执行每个提交的任务的具体实例化的ExecutorService,也就是线程池的真正实现,一般通过线程池工厂方法创建配置。
在实际使用的过程中,我们常见的用Executors来创建所需的线程池,Executors可以理解成一个线程池工厂类,用于提供多个常见线程池的构建。在没有特殊需要的情况下,我们确实可以直接使用Executors提供的4种常见线程池:
1.FixedThreadPool:作为拥有固定的线程数量(核心线程)的线程池,无非核心线程,它的核心线程无论是在工作状态还是空闲状态,都会一直留存,不会被回收,可以将其认为是以空间来换取时间的做法。
对于新增线程超出了所设定的核心线程数时,新增的线程将会被保存在阻塞队列LinkedBlockingQueue中等待,待有线程空闲时才能够取出来进入工作状态,LinkedBlockingQueue可详见——细谈Java并发】谈谈LinkedBlockingQueue
2.SingleThreadExecutor:顾名思义,它内部只包含一个核心线程,且没有非核心线程,就跟我们常见的AsyncTask调用execute一般,是一个单线程的运行模式,这样的好处是无需处理线程同步的问题,当然缺陷也是一目了然,只能是单线程阻塞进行任务的处理。
3.CachedThreadPool:没有核心线程,只有非核心线程,并且其线程数量并不固定,最大值为Integer.MAX_VALUE,且每个非核心线程都有其超时时长,超过这个时间不处于活动状态便会被销毁,当有新的任务需要提交时,如果当前线程池中的线程都处于活动状态,那么就会新建一个线程用于执行新的任务,否则如果有处于超时时间中的空闲线程,就会利用该空闲线程。
可以看到,CachedThreadPool构造中的队列与前两者不同,使用的是SynchronousQueue,这是一种无法存储元素的阻塞队列,可以参考JUC源码分析-集合篇(八):SynchronousQueue。从CachedThreadPool的这种特性中可以得出,它适宜进行大量线程的执行,且这些线程不会导致过长的耗时时间(如果耗时时间都非常长,那么结局也就可以预见了,会导致系统资源的大量消耗并会导致OOM)。
4.ScheduledThreadPool:它与CachedThreadPool有所不同的是,具有一定数量的核心线程数,且虽然其非核心线程数也是Integer.MAX_VALUE,但是不像CachedThreadPool拥有超时时间,其非核心线程一旦有空闲,就会立即回收
使用了DelayedWorkQueue,可参考Java优先级队列DelayedWorkQueue原理分析,优先级队列来控制线程任务的出入,通过任务延时调整优先级来控制其出入顺序,因此适用于执行一些定时性的周期性任务。
以上为内置的4种不同类型的线程池构造方式,已可满足一部分使用场景中所需。接下来我们介绍线程池各个构造参数的意义,助于自定义配置线程池。我们先来看看ThreadPoolExecutor的构造参数:
1.corePoolSize:顾名思义是核心线程的数量,在allowCoreThreadTimeOut为false的情况下(默认false),核心线程会一直存活,即使处于空闲状态。当allowCoreThreadTimeOut设置为true时,核心线程也跟非核心线程一样,将会有超时时间,超过超时时间,空闲的核心线程也会被回收。
2.maximumPoolSize:核心线程+非核心线程的数量,也即线程池中的最大线程数量,若超过该数量,新提交的线程会被阻塞。
3.keepAliveTime:一般而言指的是非核心线程的超时时间,当线程处于闲置状态时超出该时间,将会被回收,如上提到的若将allowCoreThreadTimeOut设为true,也可同时作用于核心线程。
4.TimeUnit:即keepAliveTime的单位,有MILLISECONDS——毫秒;SECONDS——秒;MINUTES:——HOURS——时;DAYS——天。
5.workQueue:任务队列,常见的有LinkedBlockingQueue、SynchronousQueue、DelayedWorkQueue等。
6.threadFactory:担任线程工厂的角色,默认实现为DefaultThreadFactory,如下:
可以看到,DefaultThreadFactory通过呢newThread来创建一个线程,并将线程优先级设置为NORM_PRIORITY,使用AtomicInteger原子类通过CAS来保障线程数安全。
7.RejectedExecutionHandler:字面意思为线程池的拒绝策略,当线程池的线程数达到限制时,且阻塞队列也满时,需要再提交新的任务,那么我们就需要使用到RejectedExecutionHandler来对这些不能处理的任务的处理策略。常用的策略为:
(1)AbortPolicy:该策略为默认策略,当队列已满无法添加新的任务时,会抛掉该任务,并且抛出RejectedExecutionException异常。
(2)DiscardPolicy:该策略相比AbortPolicy 不同之处在于其不会抛任何异常,直接丢弃。
(3)DiscardOldestPolicy:该策略为“喜新厌旧”策略,先进先弃,长江后浪推前浪,把前浪排死在沙滩上,形象的比拟了我们码农这个职业。
(4)DiscardOldestPolicy:该策略比较友善,线程池不执行,那么提交该线程的线程就负责去执行(一般场景下我们都在主线程提交)
因此,若是个网络请求或者耗时任务的话那么就GG,颇有种项羽乌江自刎的壮烈感。
当然,除此上述4种内置策略之外,我们也可以选择实现RejectedExecutionHandler接口去实现我们自定义的策略。
二、线程池原理
首先,我们从线程池的启动入口execute(submit内部也是调用execute)开始:
若提交的任务Runnable不为空,则会进入workerCount当前正在工作中的线程数的判定,ctl即AtomicInteger,用于保证工作线程数的原子性,若当前工作线程数小于设定的核心线程数corePoolSize,则会调用addWorker提交创建新的核心线程,我们来看看addWorker中做了什么:
未完待续...