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

转自:http://android.jobbole.com/82092/android

 

说到线程,我想你们都不陌生,由于在开发时候或多或少都会用到线程,而一般建立线程有两种方式:api

一、继承Thread类
二、实现Runnable接口缓存

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

因此,在Java中大多数多线程程序都是经过实现Runnable来完成的,而对于Android来讲也不例外,当涉及到须要开启线程去完成某件事时,咱们都会这样写:并发

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

一、线程的建立和销毁都须要时间,当有大量的线程建立和销毁时,那么这些时间的消耗则比较明显,将致使性能上的缺失异步

二、大量的线程建立、执行和销毁是很是耗cpu和内存的,这样将直接影响系统的吞吐量,致使性能急剧降低,若是内存资源占用的比较多,还极可能形成OOMasync

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

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

ExecutorService

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

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

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

二、线程池旨在线程的复用,这就能够节约咱们用以往的方式建立线程和销毁所消耗的时间,减小线程频繁调度的开销,从而节约系统资源,提升系统吞吐量

三、在执行大量异步任务时提升了性能

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

ExecutorService简介

一般来讲咱们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实若是要从真正意义上来讲,它能够叫作线程池的服务,由于它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具备线程池的特性,其中包括工做队列、核心线程数、最大线程数等。

线程池:ThreadPoolExecutor

既然线程池就是ThreadPoolExecutor,因此咱们要建立一个线程池只须要new ThreadPoolExecutor(…);就能够建立一个线程池,而若是这样建立线程池的话,咱们须要配置一堆东西,很是麻烦,咱们能够看一下它的构造方法就知道了:

因此,官方也不推荐使用这种方法来建立线程池,而是推荐使用Executors的工厂方法来建立线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不同的线程池,从而使得咱们建立线程池很是的简便,主要提供了以下五种功能不同的线程池:

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

二、newCachedThreadPool() :
做用:该方法返回一个能够根据实际状况调整线程池中线程的数量的线程池。即该线程池中的线程数量不肯定,是根据实际状况动态调整的。
栗子:假如该线程池中的全部线程都正在工做,而此时有新任务提交,那么将会建立新的线程去处理该任务,而此时假如以前有一些线程完成了任务,如今又有新任务提交,那么将不会建立新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来讲该线程池的线程岂不是会越集越多?其实并不会,由于线程池中的线程都有一个“保持活动时间”的参数,经过配置它,若是线程池中的空闲线程的空闲时间超过该“保存活动时间”则马上中止该线程,而该线程池默认的“保持活动时间”为60s。

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

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

五、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:一般叫作拒绝策略,一、在线程池已经关闭的状况下 二、任务太多致使最大线程数和任务队列已经饱和,没法再接收新的任务 。在上面两种状况下,只要知足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常

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

一、newFixedThreadPool()—>LinkedBlockingQueue
二、newSingleThreadExecutor()—>LinkedBlockingQueue
三、newCachedThreadPool()—>SynchronousQueue
四、newScheduledThreadPool()—>DelayedWorkQueue
五、newSingleThreadScheduledExecutor()—>DelayedWorkQueue

这些队列分别表示:

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

固然实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细做用就很少介绍了。

线程池ThreadPoolExecutor的使用

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

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

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

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

其实咱们经过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,建立一个singleThreadExecutorPool实际上就是建立一个核心线程数和最大线程数都为1的fixedThreadPool。
newCachedThreadPool
建立一个能够根据实际状况调整线程池中线程的数量的线程池,示例为:

为了体现该线程池能够自动根据实现状况进行线程的重用,而不是一味的建立新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,因此,效果为:
这里写图片描述
newScheduledThreadPool
建立一个能够定时或者周期性执行任务的线程池,示例为:

newSingleThreadScheduledExecutor
建立一个能够定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为:

实际上这个和上面的没什么太大区别,只不过是线程池内线程数量的不一样,效果为:
这里写图片描述
每隔2秒就会执行一次该任务

自定义线程池ThreadPoolExecutor

Java内置只为咱们提供了五种经常使用的线程池,通常来讲这足够用了,不过有时候咱们也能够根据需求来自定义咱们本身的线程池,而要自定义不一样功能的线程池,上面咱们也说了线程池功能的不一样归根到底仍是内部的BlockingQueue实现不一样,因此,咱们要实现咱们本身相要的线程池,就必须从BlockingQueue的实现上作手脚,而上面也说了BlockingQueue的实现类有多个,那么此次咱们就选用PriorityBlockingQueue来实现一个功能是按任务的优先级来处理的线程池。

一、首先咱们建立一个基于PriorityBlockingQueue实现的线程池,为了测试方便,我这里把核心线程数量设置为3,以下:

 

 

二、而后建立一个实现Runnable接口的类,并向外提供一个抽象方法供咱们实现自定义功能,并实现Comparable接口,实现这个接口主要就是进行优先级的比较,代码以下:

 

 

三、使用咱们本身的PriorityRunnable提交任务,总体代码以下:

 

 

测试效果

咱们看下刚刚自定义的线程池是否达到了咱们想要的功能,即根据任务的优先级进行优先处理任务,效果以下:
这里写图片描述

能够从执行结果中看出,因为核心线程数设置为3,刚开始时,系统有3个空闲线程,因此无须使用任务队列,而是直接运行前三个任务,然后面再提交任务时因为当前没有空闲线程因此加入任务队列中进行等待,此时,因为咱们的任务队列实现是由PriorityBlockingQueue实现的,因此进行等待的任务会通过优先级判断,优先级高的放在队列前面先处理。从效果图中也能够看到后面的任务是先执行优先级高的任务,而后依次递减。

优先级线程池的优势

从上面咱们能够得知,建立一个优先级线程池很是有用,它能够在线程池中线程数量不足或系统资源紧张时,优先处理咱们想要先处理的任务,而优先级低的则放到后面再处理,这极大改善了系统默认线程池以FIFO方式处理任务的不灵活

扩展线程池ThreadPoolExecutor

除了内置的功能外,ThreadPoolExecutor也向外提供了三个接口供咱们本身扩展知足咱们需求的线程池,这三个接口分别是:

beforeExecute() – 任务执行前执行的方法
afterExecute() -任务执行结束后执行的方法
terminated() -线程池关闭后执行的方法

这三个方法在ThreadPoolExecutor内部都没有实现

前面两个方法咱们能够在ThreadPoolExecutor内部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的内部类Worker实现的方法,Worker它实现了Runnable接口,也正是线程池内处理任务的工做线程,而Worker.runWorker()方法则是处理咱们所提交的任务的方法,它会同时被多个线程访问,因此咱们看runWorker()方法的实现,因为涉及到多个线程的异步调用,必然是须要使用锁来处理,而这里使用的是Lock来实现的,咱们来看看runWorker()方法内主要实现:
这里写图片描述

能够看到在task.run()以前和以后分别调用了beforeExecute和afterExecute方法,并传入了咱们的任务Runnable对象

而terminated()则是在关闭线程池的方法中调用,而关闭线程池有两个方法,我贴其中一个:
这里写图片描述

因此,咱们要扩展线程池,只须要重写这三个方法,并实现咱们本身的功能便可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。
这里我验证一下,继承自ThreadPoolExecutor 并实现那三个方法:

而运行后的结果则是,这正符合刚刚说的:

因此,在上面咱们的优先级线程池的代码上,咱们再扩展一个具备暂停功能的优先级线程池,代码以下:
具备暂时功能的线程池:

而后结合上面的优先级线程池的实现,建立具备暂停功能的优先级线程池:

这里我为了演示效果,把这个线程池设为只有一个线程,而后直接在TextView中显示当前执行的任务的优先级,而后设置个开关,控制线程池的暂停与开始:

效果为:
这里写图片描述

从效果上来看,该线程池和优先级线程同样,并且还多了一个暂停与开始的功能

优化线程池ThreadPoolExecutor

虽然说线程池极大改善了系统的性能,不过建立线程池也是须要资源的,因此线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,因此咱们建立线程池须要把握一个度才能合理的发挥它的优势,一般来讲咱们要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。

一般核心线程数能够设为CPU数量+1,而最大线程数能够设为CPU的数量*2+1。

获取CPU数量的方法为:

 

shutdown()和shutdownNow()的区别

关于线程池的中止,ExecutorService为咱们提供了两个方法:shutdown和shutdownNow,这两个方法各有不一样,能够根据实际需求方便的运用,以下:

一、shutdown()方法在终止前容许执行之前提交的任务。
二、shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图中止当前正在执行的任务。

关于AsyncTask的实现

你们都知道AsyncTask内部实现其实就是Thread+Handler。其中Handler是为了处理线程之间的通讯,而这个Thread究竟是指什么呢?经过AsyncTask源码能够得知,其实这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是经过CPU的数量决定的。

在默认状况下,咱们大都经过AsyncTask::execute()来执行任务的,
,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认状况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行,而咱们要是想让AsyncTask并行的处理任务,你们都知道调用AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数便可:AsyncTask.THREAD_POOL_EXECUTOR。
而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,咱们能够在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:

相关文章
相关标签/搜索