Object-c

多线程(三)-为主线程减负的多线程方案

2019-11-21  本文已影响0人  Stan_Z

再梳理一篇多线程的文章,正好续上之前的多线程章节,完善下这个章节内容。

一、多线程方案:

ThreadPool: 把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。
AsyncTask: 为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。
HandlerThread: 为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。
IntentService: 适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。

二、逐一分析
2.1 ThreadPool

适用场景:任务量相对大,需要把任务进行分解,并发进行执行的场景。
优点:线程池的优点是相比较单独创建Thread而言的。

BlockingQueue阻塞队列介绍
阻塞队列常用于生产者消费者场景,常用的阻塞队列如下:

最常用的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<Runnable> workQueue)//任务队列

常用的线程池4类:

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(
            nThreads, //核心线程固定
           nThreads, // 最大线程 = 核心线程
           0L, //那么自然也不需要非核心线程闲置的超时时间
           TimeUnit.MILLISECONDS,
           new LinkedBlockingQueue<Runnable>());//无界阻塞队列
}

总结:FixedThreadPool就是固定核心线程的线程池,无非核心线程,并且核心线程不会被回收,当执行execute方法时,未达到核心线程数,直接取空闲线程用,达到核心线程数加入无界阻塞队列等待空闲线程释放。
适用场景:低并发但每个任务耗时较长的场景。

FixedThreadPool执行示意图

CacheThreadPool

public static ExecutorService newCacheThreadPool(){
    return new ThreadPoolExecutor(
            0, //无核心线程
           Integer.MAX_VALUE, //非核心线程无限制
           60L,//非核心线程闲置的超时时间为60s
           TimeUnit.SECONDS,
           new SynchronousQueue<Runnable>());//不存储元素的阻塞队列,出一个才能进一个。
}

总结:当执行execute方法时,向SynchronousQueue提交任务,此时SynchronousQueue需要移除一个任务去被执行,如果此时有空闲线程则交给空闲线程处理,没有则新建线程处理。空闲线程超过60s没被使用则回收。
适用场景:高并发但每个任务耗时较少的场景。但是实际上一般都不会用,因为非核心线程无限制,如果一直没有空闲线程,一直不停创建新的线程,很容易就OOM了。

CacheThreadPool执行示意图

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中。

适用场景:有周期性任务和定时需求。

ScheduledThreadPool执行示意图

SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
   return new FinalizableDelegatedExecutorService
       (new ThreadPoolExecutor(1, 1,
                               0L, TimeUnit.MILLISECONDS,
                               new LinkedBlockingQueue<Runnable>()));
}
SingleThreadExecutor执行示意图

总结:执行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<Runnable> sPoolWorkQueue =
       new LinkedBlockingQueue<Runnable>(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,”")声明使用并行处理所使用的线程池。

优点:提供一种非常简单的异步处理机制。
缺点:

AsyncTask执行示意图

综上分析:单一小任务就用默认串行处理就行,稍微多一点的并发任务可以用打开并行线程池来处理,但是大量并发就不行了,很明显它对应的线程池的线程是有限的,并不像CacheThreadPool那样。

2.3 HandlerThread

这个比较简单,HandlerThread 就是拥有looper的子线程 ,配合Handler挂上HandlerThread对应的loop来使用。让大量任务工作在子线程且排队执行。

使用场景:
HandlerThread比较合适处理那些在工作线程执行,但是花费时间不长的任务(这里Android性能优化典范(五)中写的是花费时间长的任务,我有不同意见,见缺点分析)。我们只需要把任务发送给HandlerThread,然后就只需要等待任务执行结束的时候通知返回到主线程就好了。

优点:

缺点:任务在工作线程串行执行,那么前面任务耗时就会delay到后面的消息处理,虽然是在子线程,但是有些需要同步到主线程的操作就会受到影响。

HandlerThread执行示意图
2.4 IntentService:

默认的Service是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的AsyncTask与HandlerThread,我们还可以选择使用IntentService来实现异步操作。IntentService继承自普通Service同时又在内部创建了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。所以IntentService就不仅仅具备了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特点。

特点:

适用场景:它的本质是在子线程执行任务的服务,因此适合做耗时操作。

IntentService执行示意图

最后结合自己这一波的学习做个总结:

AsyncTask:单一、耗时少的任务且需要返回UI线程做操作的小活可以用,啥都给你封装好了,照着对应方法填就行,另外做进度条也很方便。
HandlerThread:执行在工作线程,同时又能够处理队列中的复杂任务,可以简单决定执行顺序,且多组子线程任务直接还可以设置线程优先级的。它在Android系统中大量使用,什么UIThread、IOThread统统都是HandlerThread。为各种系统服务提供对应的子线程服务。
IntentService:它就是封装好的子线程工作的服务,使用服务的地方可以考虑,一般简单的异步任务就别杀鸡用牛刀起服务去干了。

线程池:
FixedThreadPool:并发不高但每个任务耗时较长的场景用。
CacheThreadPool:不用,易发生OOM。
ScheduledThreadPool:周期性执行和定时需求的场景。
SingleThreadExecutor:这个感觉Android中用得应该不多,它能实现的HandlerThread都能实现。可能纯java会比较有使用场景吧。

好了,就写这么多,欢迎针对我的个人观点进行讨论与批评指正。我靠,又凌晨了,狗命要紧,睡觉!

参考:
线程池之ScheduledThreadPool学习
线程池之FixedThreadPool学习
线程池之CachedThreadPool学习
线程池之SingleThreadPool学习
Android性能优化典范(五)

上一篇下一篇

猜你喜欢

热点阅读