Android 性能优化之使用线程池处理异步任务

666 查看

说到线程,我想大家都不陌生,因为在开发时候或多或少都会用到线程,而通常创建线程有两种方式:

1、继承Thread类
2、实现Runnable接口

虽说这两种方式都可以创建出一个线程,不过它们之间还是有一点区别的,主要区别在于在多线程访问同一资源的情况下,用Runnable接口创建的线程可以处理同一资源,而用Thread类创建的线程则各自独立处理,各自拥有自己的资源。

所以,在Java中大多数多线程程序都是通过实现Runnable来完成的,而对于Android来说也不例外,当涉及到需要开启线程去完成某件事时,我们都会这样写:

这段代码创建了一个线程并执行,它在任务结束后GC会自动回收该线程,一切看起来如此美妙,是的,它在线程并发不多的程序中确实不错,而假如这个程序有很多地方需要开启大量线程来处理任务,那么如果还是用上述的方式去创建线程处理的话,那么将导致系统的性能表现的非常糟糕,更别说在内存有限的移动设备上,主要的影响如下:

1、线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失

2、大量的线程创建、执行和销毁是非常耗cpu和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM

3、大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿

而针对上述所描述的问题,解决的办法归根到底就是:重用已有的线程,从而减少线程的创建。
所以这就涉及到线程池(ExecutorService)的概念了,线程池的基本作用就是进行线程的复用,下面将具体介绍线程池的使用

ExecutorService

通过上述分析,我们知道了通过new Thread().start()方式创建线程去处理任务的弊端,而为了解决这些问题,Java为我们提供了ExecutorService线程池来优化和管理线程的使用

使用线程池管理线程的优点

1、线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销

2、线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量

3、在执行大量异步任务时提高了性能

4、Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等

ExecutorService简介

通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。

线程池:ThreadPoolExecutor

既然线程池就是ThreadPoolExecutor,所以我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池,而如果这样创建线程池的话,我们需要配置一堆东西,非常麻烦,我们可以看一下它的构造方法就知道了:

所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:

1、newFixedThreadPool() :
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
栗子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。

2、newCachedThreadPool() :
作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
栗子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。

3、newSingleThreadExecutor() :
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。

4、newScheduledThreadPool() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。

5、newSingleThreadScheduledExecutor() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。

好了,写了一堆来介绍这五种线程池的作用,接下来就是获取这五种线程池,通过Executors的工厂方法来获取:

我们可以看到通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:

我们可以清楚的看到这些方法的内部实现都是通过创建一个ThreadPoolExecutor对象来创建的,正所谓万变不离其宗,所以我们要了解线程池还是得了解ThreadPoolExecutor这个线程池类,其中由于和定时任务相关的线程池比较特殊(newScheduledThreadPool()、newSingleThreadScheduledExecutor()),它们创建的线程池内部实现是由ScheduledThreadPoolExecutor这个类实现的,而ScheduledThreadPoolExecutor是继承于ThreadPoolExecutor扩展而成的,所以本质还是一样的,只不过多封装了一些定时任务相关的api,所以我们主要就是要了解ThreadPoolExecutor,从构造方法开始:

我们可以看到它构造方法的参数比较多,有七个,下面一一来说明这些参数的作用:

corePoolSize:线程池中的核心线程数量
maximumPoolSize:线程池中的最大线程数量
keepAliveTime:这个就是上面说到的“保持活动时间“,上面只是大概说明了一下它的作用,不过它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中
unit:它是一个枚举类型,表示keepAliveTime的单位,常用的如:TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)
workQueue:任务队列,主要用来存储已经提交但未被执行的任务,不同的线程池采用的排队策略不一样,稍后再讲
threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可
handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常

上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,下面我总结一下这五种线程池分别用的是什么BlockingQueue:

1、newFixedThreadPool()—>LinkedBlockingQueue
2、newSingleThreadExecutor()—>LinkedBlockingQueue
3、newCachedThreadPool()—>SynchronousQueue
4、newScheduledThreadPool()—>DelayedWorkQueue
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue

这些队列分别表示:

LinkedBlockingQueue:无界的队列
SynchronousQueue:直接提交的队列
DelayedWorkQueue:等待队列

当然实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细作用就不多介绍了。

线程池ThreadPoolExecutor的使用

使用线程池,其中涉及到一个极其重要的方法,即:

该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。
newFixedThreadPool
创建一个固定线程数量的线程池,示例为:

上述代码,我们创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。
为了体现出线程的复用,我特地在Log中加上了当前线程的名称,效果为:
这里写图片描述
newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:

代码还是差不多,只不过改了线程池的实现方式,效果我想大家都知道,即依次一个一个的处理任务,而且都是复用一个线程,效果为:
这里写图片描述

其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。
newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池,示例为: