About ExecutorService(2),自定义线程池
About ExecutorService(1),Future&FutureTask
About ExecutorService(2),自定义线程池
About ExecutorService(3),我所认识的AsyncTask
About ExecutorService(4),AsyncTask番外篇
琢磨了一下,还是把这篇提前了,本片篇幅可能会有些长,甚至冗余,请各位看官原谅我这拙劣的写作能力。
本篇的重点是,简单线程池的实现,Executor框架,优化后的自定义线程池。
Android主线程不是线程安全的,不能阻塞太久,否则会报ANR异常,于是,我们经常这样做来处理耗时操作,查询数据,甚至联网请求。
new Thread(new Runnable() {
@Override
public void run() {
/*处理耗时操作*/
}
}).start();
这段看似常用,而且大家都在用,其实蕴藏着很多学问。那么我就为大家解释一下。
这段代码首先创建了一个线程,并在run( )
方法结束后,系统自动回收该线程,可以说在简单的应用中,没什么问题,但是如果放到复杂的生产环境中,系统由于真实环境的需要,可能会开启很多线程来做支撑。而当线程数量过大时,反而会消耗CUP和内存资源。
与进程相比,线程算的上是一个轻量级的工具,但是!!!其创建和关闭都需要花费时间和资源,频繁的创建子线程,很可能出现造成创建和销毁线程所花的时间要比业务逻辑的工作时间还要长的悲剧,这样就得不偿失了。
Android的垃圾回收机制是基于虚拟机的,虽智能,但不够完美,子线程频繁创建和销长毁,依然会抢夺资源,不仅给GC带来压力,严重的时候甚至会报OOM异常。
所以,应该对子线程的使用掌握一个度,接下来,duang,duang,duang,给大家带来线程池的简单实现。
线程池的基本功能就是进行线程的复用,由于篇幅原因,我把它提前发布出来并附上链接。
通过上面链接的小例子,是不是对线程池的构造有了一个初步的了解。接下来为大家介绍一下系统自带的Executor框架,为开发者们自定义线程池带来了极大的方便。
可以说Executors是一个工厂类,里面有许多静态方法,供开发者调用。
/*该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。
* 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
* 默认等待队列长度为Integer.MAX_VALUE*/
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
/*该方法返回一个只有一个线程的线程池。
* 若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出顺序执行队列中的任务
* 默认等待队列长度为Integer.MAX_VALUE*/
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
/*该方法返回一个可根据实际情况调整线程数量的线程池。
* 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。
* 若所有线程均在工作,又有新任务的提交,则会创建新的线程处理任务。
* 所有线程在当前任务执行完毕后,将返回线程池进行复用*/
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
/*该方法返回一个ScheduledExecutorService对象,线程池大小为1。
* ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间内执行某任务的功能,
* 如在某个固定的延时之后执行,或者周期性执行某个任务*/
ExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
/*该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量*/
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);
如果童鞋们认为,Executors工厂类提供的自定义线程池,还不够用的话,也可以自定义线程池。通过ThreadPoolExecutor的构造函数自定义需要的线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空余线程存活时间,即,超过corePoolSize的空闲线程,在多长时间内被销毁。如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
unit:keepAliveTime的单位。
workQueue:任务队列,被提交但尚未被执行任务的任务。有以下几种队列模式:
-
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
-
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
-
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
-
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。也可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。
handler:拒绝策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。系统内置的策略模式如下:
-
AbortPolicy:直接抛出异常,阻止系统正常工作。
-
CallerRunsPolicy:只要线程未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
-
DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的一个人任务,并尝试再次提交当前任务。
-
DiscardPolicy:该策略默默的丢弃无法处理对
-
当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。
好像好久没说到AT了,那么接下来我们根据源码和画图的方式分析一下AT中到底构造了一个如何的线程池
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
为了保持处理器达到期望的使用率,最优线程池大小,终于在一本叫做《Java并发编程实践》的书中找到了估算线程池大小的经验值:Nthread。
Nthread = Ncpu * Ucpu * (1 + W/C)
其中,Ncpu = CPU的数量,Ucpu = 目标CPU的使用量,0 <= Ucpu <=1,W/C = 等待时间与计算时间的比率。
由于各种八核手机出现,以及硬件的提升,这个公式貌似也不常用了。大家了解即可。
接下来介绍AT中线程池的工作原理
1,假如我手中用了双核手机(两个CPU),初始状态下线程池的工作线程为0,当第一个任务进来时,会创建工作线程。则根据AT中计算CPU_COUNT = 2,CORE_POOL_SIZE = 3 ,红色代表新任务,当线程池中的工作线程池数量已经达到了CORE_POOL_SIZE,下个任务,如果工作线程没有空闲的话,必将进入缓冲队列中,进行排队。
2,线程池中没有空闲线程,新的任务要进入缓冲队列进行排队。
3,缓冲队列也满了,线程池中的工作线程依然没有空闲。
通过AT中的计算,MAXIMUM_POOL_SIZE = 5。
当线程池工作线程达到CORE_POOL_SIZE并且没有空闲,缓冲队列任务数量达到AT中设定的128,
此时新的任务进入线程池之前,如果线程池中工作线程数量小于MAXIMUM_POOL_SIZE,则创建新的工作线程执行任务。如下图,线程池创建第4个工作线程,执行第129个任务。
4.线程池工作线程数量等于MAXIMUM_POOL_SIZE,缓冲队列数量等于最大workQuene最大值,并且没有工作线程处于空闲,第131任务进入线程池的时候,拒绝策略handler发挥作用,AT中默认的拒绝策略为抛出RuntimeException异常。(RuntimeException和Error同属于UncaughtException,可以在Application中进行捕获和处理)
由上述步骤,不难发现workQueue应该避免无边界队列,尽量使用带边界的,如ArrayBlockingQueue和固定capacity容量的LinkedBlockingQueue。否则线程池的缓冲队列将是一个无边界的队列,任务过多而不能执行拒绝策略的情况下,可能会撑满内存,导致整个系统不可用,造成不可预估的后果。
片尾Tip:
最近有群里的朋友抱怨,Android studio 编译速度太慢,太卡了,交给大家一个小技巧,我的电脑还是公司的老款戴尔,至于你信不信,反正我是信了,它确实快了。