再梳理一篇多线程的文章,正好续上之前的多线程章节,完善下这个章节内容。
一、多线程方案:
ThreadPool
: 把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
AsyncTask
: 为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。
HandlerThread
: 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
IntentService
: 适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。
二、逐一分析
2.1 ThreadPool
适用场景:任务量相对大,需要把任务进行分解,并发进行执行的场景。
优点:线程池的优点是相比较单独创建Thread而言的。
- 线程复用减少开销。
- 对线程统一管理,有效控制最大并发线程数。
- 提供更多功能,比如定时执行、定期执行、单线程等。
BlockingQueue阻塞队列介绍:
阻塞队列常用于生产者消费者场景,常用的阻塞队列如下:
-
ArrayBlockingQueue
数组实现的有界阻塞队列。 -
LinkedBlockingQueue
链表实现的有界阻塞队列。 -
DelayQueue
使用优先级队列实现的无界阻塞队列。 -
PriorityBlockingQueue
支持优先级排序的无界阻塞队列。 -
SynchronousQueue
不存储元素的阻塞队列,进一个元素必先出一个。 -
LinkedTransferQueue
链表实现的无界阻塞队列。 -
LinkedBlockingDeque
链表实现的双向阻塞队列。
最常用的ArrayBlockingQueue和LinkedBlockingQueue区别:
队列中锁的实现不同:
ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
在生产或消费时操作不同:
ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
队列大小初始化方式不同:
ArrayBlockingQueue实现的队列中必须指定队列的大小;
LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
好,简单了解阻塞队列之后,接下来了解线程池,先来看下线程池的处理流程:
线程池的分类介绍:
线程池就是通过设置ThreadPoolExecutor的不同参数来创建不同类型的线程池,那先来了解下参数:
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize,//线程池允许创建的最大线程数
long keepAliveTime,//非核心线程闲置的超时时间
TimeUnit unit,//keepAliveTime的时间单位
BlockingQueue workQueue)//任务队列
常用的线程池4类:
FixedThreadPool
:
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(
nThreads, //核心线程固定
nThreads, // 最大线程 = 核心线程
0L, //那么自然也不需要非核心线程闲置的超时时间
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque());//无界阻塞队列
}
总结:FixedThreadPool就是固定核心线程的线程池,无非核心线程,并且核心线程不会被回收,当执行execute方法时,未达到核心线程数,直接取空闲线程用,达到核心线程数加入无界阻塞队列等待空闲线程释放。
适用场景:低并发但每个任务耗时较长的场景。
CacheThreadPool
:
public static ExecutorService newCacheThreadPool(){
return new ThreadPoolExecutor(
0, //无核心线程
Integer.MAX_VALUE, //非核心线程无限制
60L,//非核心线程闲置的超时时间为60s
TimeUnit.SECONDS,
new SynchronousQueue());//不存储元素的阻塞队列,出一个才能进一个。
}
总结:当执行execute方法时,向SynchronousQueue提交任务,此时SynchronousQueue需要移除一个任务去被执行,如果此时有空闲线程则交给空闲线程处理,没有则新建线程处理。空闲线程超过60s没被使用则回收。
适用场景:高并发但每个任务耗时较少的场景。但是实际上一般都不会用,因为非核心线程无限制,如果一直没有空闲线程,一直不停创建新的线程,很容易就OOM了。
ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize,
Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, //10L
MILLISECONDS,
new DelayedWorkQueue());
}
ScheduledThreadPool是一个能实现定时和周期性任务的线程池,从配置上看它是定时版的FixedThreadPool。
总结:当执行ScheduledThreadPoolExecutor的scheduleAtFixedRate或scheduleWithFixedDelay方法,会向DelayedWorkQueue添加一个实现RunnableScheduledFuture接口的任务包装类ScheduledFutureTask,并检查运行的线程是否达到核心线程数corePoolSize。如果没有就新建线程,并启动。但并非立即执行任务,而是去DelayedWorkQueue中取任务包装类ScheduledFutureTask,然后再去执行任务;如果运行的线程达到了corePoolSize,就把任务添加到任务队列DelayedWorkQueue中;DelayedWorkQueue会将任务排序,先执行的任务放在队列的前面。任务执行完后,ScheduledFutureTask中的变量time改为下次要执行的时间,并放回到DelayedWorkQueue中。
适用场景:有周期性任务和定时需求。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
总结:执行execute方法时,若当前运行的线程数未达到核心线程数(没有正在运行的线程),就创建一个新线程来处理任务;如果当前有运行的线程,就把任务添加到阻塞队列LinkedBlockingQueue。SingleThreadPool能够确保所有的任务都在一个线程中按照顺序逐一执行。
适用场景:需要任务在一个线程中按顺序执行。
2.2 AsyncTask
适用场景:任务尽量单一的且异步执行的生命周期短暂的场景。
技术特点:
- 线程池:
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final BlockingQueue sPoolWorkQueue =
new LinkedBlockingQueue(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, //核心线程2-4
MAXIMUM_POOL_SIZE, //最大线程CPU_COUNT * 2 + 1,8核CPU就是17,非核心线程就是13-15
KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,//非核心线程超时30S
sPoolWorkQueue, //最大为128容量的LinkedBlockingQueue
sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
这就是通过AsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,”")声明使用并行处理所使用的线程池。
工作线程实现是:FutureTask+Callable的方式,这种方式的执行有返回值。
使用handler来切换UI线程。
优点:提供一种非常简单的异步处理机制。
缺点:
- Android 3.0之后默认情况下,任务是串行处理,一旦其中的某个AsyncTask执行时间过长,队列中的其他剩余AsyncTask都处于阻塞状态,必须等到该任务执行完毕之后才能够有机会执行下一个任务。不过可以使用AsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,”")强制指定AsyncTask使用线程池并发调度任务。
- 终止工作线程任务比较麻烦,虽然AsyncTaks有提供cancel()的方法,但是它并不是直接终止工作线程的执行,而是作为一个flag配合在doInBackground()的代码中不断的添加程序是否被中止的判断逻辑。一旦任务被成功中止,AsyncTask就不会继续调用onPostExecute(),而是回调onCancelled()。
- AsyncTask经常被作为内部类使用,很容易造成内部类持有外部类引用造成内存短暂泄漏的情况。
综上分析:单一小任务就用默认串行处理就行,稍微多一点的并发任务可以用打开并行线程池来处理,但是大量并发就不行了,很明显它对应的线程池的线程是有限的,并不像CacheThreadPool那样。
2.3 HandlerThread
这个比较简单,HandlerThread 就是拥有looper的子线程 ,配合Handler挂上HandlerThread对应的loop来使用。让大量任务工作在子线程且排队执行。
使用场景:
HandlerThread比较合适处理那些在工作线程执行,但是花费时间不长的任务(这里Android性能优化典范(五)中写的是花费时间长的任务,我有不同意见,见缺点分析)。我们只需要把任务发送给HandlerThread,然后就只需要等待任务执行结束的时候通知返回到主线程就好了。
优点:
- 单一工作线程对多任务进行串行管理最简单高效的实现,当然你也可以使用SingleThreadPool,但是handler发送消息的串行可以按时间来排序,顺序可以调整,而SingleThreadPool应该就不能排序了,而AsyncTask的串行方式同样面临这个问题,并且AsyncTask如果是串行,那么后面被阻塞的任务是被阻塞在主线程。
- 可以给HandlerThread设置不同的线程优先级,CPU会根据设置的不同线程优先级对所有的线程进行调度优化。
缺点:任务在工作线程串行执行,那么前面任务耗时就会delay到后面的消息处理,虽然是在子线程,但是有些需要同步到主线程的操作就会受到影响。
2.4 IntentService:
默认的Service是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的AsyncTask与HandlerThread,我们还可以选择使用IntentService来实现异步操作。IntentService继承自普通Service同时又在内部创建了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。所以IntentService就不仅仅具备了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特点。
特点:
- 因为IntentService内置的是HandlerThread作为异步线程,所以每一个交给IntentService的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。
- 通常使用到IntentService的时候,我们会结合使用BroadcastReceiver把工作线程的任务执行结果返回给主UI线程。使用广播容易引起性能问题,我们可以使用LocalBroadcastManager来发送只在程序内部传递的广播,从而提升广播的性能。我们也可以使用runOnUiThread()快速回调到主UI线程。
- 包含正在运行的IntentService的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。
适用场景:它的本质是在子线程执行任务的服务,因此适合做耗时操作。
最后结合自己这一波的学习做个总结:
AsyncTask
:单一、耗时少的任务且需要返回UI线程做操作的小活可以用,啥都给你封装好了,照着对应方法填就行,另外做进度条也很方便。
HandlerThread
:执行在工作线程,同时又能够处理队列中的复杂任务,可以简单决定执行顺序,且多组子线程任务直接还可以设置线程优先级的。它在Android系统中大量使用,什么UIThread、IOThread统统都是HandlerThread。为各种系统服务提供对应的子线程服务。
IntentService
:它就是封装好的子线程工作的服务,使用服务的地方可以考虑,一般简单的异步任务就别杀鸡用牛刀起服务去干了。
线程池:
FixedThreadPool
:并发不高但每个任务耗时较长的场景用。
CacheThreadPool
:不用,易发生OOM。
ScheduledThreadPool
:周期性执行和定时需求的场景。
SingleThreadExecutor
:这个感觉Android中用得应该不多,它能实现的HandlerThread都能实现。可能纯java会比较有使用场景吧。
好了,就写这么多,欢迎针对我的个人观点进行讨论与批评指正。我靠,又凌晨了,狗命要紧,睡觉!
参考:
线程池之ScheduledThreadPool学习
线程池之FixedThreadPool学习
线程池之CachedThreadPool学习
线程池之SingleThreadPool学习
Android性能优化典范(五)