Android线程池分析与使用——让你的App更高效

什么是线程池

线程池是多个线程的集合,一旦有任务传给线程池,并且线程池中还有空闲线程的情况下,就会启动空闲线程执行该任务,执行结束之后,线程变为空闲状态等待下一个任务的执行。


ThreadPool.png

 

为什么要使用线程池?

因为不断地创建线程销毁线程,会占用CPU的资源,减少CPU做其他有效工作的时间。线程池里的每一个线程任务结束后,并不会销毁,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率。同时还可以控制线程的并发数量,避免大量并发导致内存溢出。
 

线程池的使用方式

在Java中创建一个线程池是基于ThreadPoolExecutor的构造方法,构造方法参数如下:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

可以看出,构造方法需要配置多个参数,它们各自代表的意义如下:

corePoolSize 线程池的核心线程数量。如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize 线程池的最大线程数量,即最多能创建多少个线程
keepAliveTime 线程在没有任务执行之后多久销毁
unit 这个就是keepAliveTime的单位,比如秒、分、小时,详见TimeUnit
workQueue 用来存储等待执行任务的队列,当线程的数量超过corePoolSize时,会被加入到这个队列当中等待
threadFactory 用来创建线程
handler 当线程池中线程数量超出 maximumPoolSize 时的处理策略

当执行一个新的任务时,它们之间的流程如下:

检查当前线程池的数量是否小于corePoolSize,如果小于corePoolSize,就会创建一个新的线程处理该任务。如果大于等于corePoolSize,但缓冲队列workQueue还未满,就将该任务加入缓冲队列,如果缓冲队列有限定大小,且当前已达到队列的上限,就会检查当前线程数量,如果小于最大线程数maximumPoolSize,就会创建新的线程,如果大于等于maximumPoolSize,就会通过handler所指定的拒绝策略来执行处理。

如下,通过ThreadPoolExecutor创建一个简单的线程池并执行任务:

val pool = ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,SynchronousQueue(),ThreadPoolExecutor.DiscardPolicy())
mPool.execute(Runnable {
    //...执行任务
})

 

线程队列

线程池采用的队列是Java中的阻塞队列BlockingQueueBlockingQueue 是一个接口,它的实现类有 ArrayBlockingQueueDelayQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue 等。一般线程池常用的队列类型主要有 ArrayBlockingQueueLinkedBlockingDequeSynchronousQueue

线程池常用的缓存队列有以下几种:

ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,所以需要在初始化时指定队列的大小。出队和入队共用同一个锁。

LinkedBlockingQueue

基于链表的阻塞队列实现,出队和进队分别采用独立的锁来控制数据同步,可以并行地操作入队和出队操作,提高整个队列的并发性能。如果初始化的时候没有指定大小,则默认为Integer.MAX_VALUE的大小。

SynchronousQueue

没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列。采用这种队列时maximumPoolSizes一般需要指定为Integer.MAX_VALUE,否则可能会直接执行拒绝策略。

 

拒绝策略

前面说了,如果线程池的线程数量超过maximumPoolSizes的大小,就会使用handler参数所配置的拒绝策略去处理,handler是一个RejectedExecutionHandler类型的对象,而RejectedExecutionHandler是一个接口,它有以下四个具体的实现类

ThreadPoolExecutor.AbortPolicy

采用此策略时线程池会丢弃当前任务,同时抛出一个RejectedExecutionException的异常,打断当前的执行流程,同时这也是线程池默认的拒绝策略。

ThreadPoolExecutor.CallerRunsPolicy

采用此策略时会直接在 execute 方法调用时所处的线程中运行被拒绝的任务。

ThreadPoolExecutor.DiscardPolicy

采用此策略时会直接丢弃该任务。

ThreadPoolExecutor.DiscardOldestPolicy

采用此策略时,只要线程池没有关闭的话,丢弃队列中最先进去的一个任务,把最新的任务加入队列。

 

线程池的关闭

我们都知道线程有线程的中断方式,通过interrrupt去安全地处理一个线程的中断,线程池也有线程池的关闭方式,Java为我们提供了shutdownNowshuwdown方法。

shutdownNow:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
shutdown:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

在讨论这两个方法之前,先了解一下线程池有哪些状态:

RUNNING 接受新任务,并且处理队列任务的状态
SHUTDOWN 不接受新任务,但是会处理队列任务的状态
STOP 不接受新任务,并且也不会处理队列任务的状态
TIDYING 所有线程池内线程都将被终止,并且将workCount清零,会运行终止线程池的方法
TERMINATED 运行终止线程池方法以及结束的状态

shutdownNow

public List shutdownNow() {
        List tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
}
private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
}

查看 shutdownNow 的源码可以看到先将线程池的状态标记为STOP,然后再将当前线程池中的所有线程逐一调用 interrupt 方法。

shutdown

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
}
private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

可以看到,跟 shutdownNow 的区别在于设置的状态是SHUTDOWN,然后同样遍历调用线程的 interrupt 方法,但是有一个关键点,这里判断了 tryLocktryLock 是尝试为线程加锁,返回true说明加锁成功,才会进一步调用 interrupt 方法,而线程池中只有正在运行的线程,会被lock住,所以正在运行的线程 tryLock 是会返回false的,也就不会被立即中断了。

所以综上两种关闭方法,我们需要根据不同的场景选择不同的处理方式,如果是选择 shutdown 关闭线程池,需要确认所有任务中没有会造成永久阻塞的场景,否则就会由于一直无法中断而导致无法关闭线程池。如果是采用 shutdownNow 的方式,则可能会由于突然中断抛出异常,需要进行对应的捕获处理。
 

四种常见的线程池

除了以上的自主配置的线程池,Java也为我们提供了几种常见的线程池,各自适用于一些常见的场景,如下:

CachedThreadPool:调用Executors.newCachedThreadPool()创建
FixedThreadPool:调用Executors.newFixedThreadPool()创建
ScheduledThreadPool:调用Executors.newScheduledThreadPool()创建
SingleThreadExecutor:调用Executors.newSingleThreadExecutor()创建

newCachedThreadPool

作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,其构造参数如下:

public static Executors newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
}

原理:CachedThreadPool 的 corePoolSize 被设置为 0,maximumPoolSize 被设置为 Integer.MAX_VALUE,keepAliveTime 设置为 60,意味着 CachedThreadPool 中的空闲线程等待新任务的最长时间是 60 秒,空闲线程超过 60 秒后将会被终止。CachedThreadPool 使用没有容量的 SynchronousQueue 作为线程池的工作队列,但CachedThreadPool 的 maximumPool 是无界的。所以每次有新的任务进来,线程池由于corePoolSize为0,会不断将任务加入队列,但由于队列没有容量,但maximumPool无限大,所以进而不断创建新的线程去执行任务,且超过60秒空闲的话就销毁。

newFixedThreadPool

作用:创建一个可重用固定线程数的线程池,其构造参数如下:

public static Executors newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

原理:FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置成了相同的数量,keepAliveTime设置为0,以LinkedBlockingQueue作为缓存队列,但没有设置队列大小所以默认是无限大,所以每次有新的任务进来,如果小于corePoolSize会创建新的线程来执行任务,如果大于corePoolSize会不断将其加入到队列里面,直到有新的线程来执行队列中的任务。

newScheduledThreadPool

作用:创建一个可以延时或者定期执行任务的线程池,其构造参数如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

原理:传入一个参数设置核心线程数的大小,且使用DelayedWorkQueue作为任务队列,DelayedWorkQueue基于最小堆构造(父节点小于等于子节点,根节点元素是所有元素中最小的一个),可以看执行时间优先级排列,使得添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被获取。

newSingleThreadExecutor

作用:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

原理:可以看到核心线程数和最大线程数都是1,所以这种线程池最多只会同时存在一个线程,且采用无限大小的LinkedBlockingQueue队列,所以每次有新的任务进来,如果小于corePoolSize会创建一个线程来执行任务,如果大于corePoolSize会不断将其加入到队列里面,直到刚才那个线程执行完毕,才会从队列中取出下一个任务执行。
 

结语

合理地利用线程池处理多线程并发的场景,可以大大提高线程的复用率,也可以很方便地统一管理各个线程的生成和销毁,在Android中也有一些场景很适合用线程池去调度,比如一个有很多下载任务的下载列表,比如一组高频读写的数据库操作等等,灵活运用。
 

欢迎关注 Android小Y 的,更多Android精选原创

『Android自定义View实战』实现一个小清新的弹出式圆环菜单
『Android自定义View实战』玩转PathMeasure之自定义支付结果动画
『Android自定义View实战』自定义炫酷侧滑解锁效果
『Android自定义View实战』Android自定义带侧滑菜单的二维表格组件

GitHub:GitHubZJY
简 书:Android小Y
在GitHub上搭建了一个集合炫酷自定义View的项目 ZJYWidget ,里面有很多实用的自定义View源码及demo,会长期维护,欢迎Star~ 如有不足之处或建议还望指正,相互学习,相互进步,如果觉得不错动动小手点个喜欢, 谢谢~

你可能感兴趣的:(Android线程池分析与使用——让你的App更高效)