Android多线程编程之线程池学习篇(一)

Android多线程编程之线程池学习篇(一)

一、前言

Android应用开发中多线程编程应用比较广泛,而应用比较多的是ThreadPoolExecutor,AsyncTask,IntentService,HandlerThread,AsyncTaskLoader等,为了更详细的分析每一种实现方式,将单独成篇分析。后续篇章中可能涉及到线程池的知识,特此本篇分析为何使用线程池,如何使用线程池以及线程池的使用原理。

二、Thread Pool基础

进程代表一个运行中的程序,一个运行中的Android应用程序就是一个进程。从操作系统的方面来说,线程是进程中可以独立执行的子任务。一个进程可以有多个线程,同一个进程中的线程可以共享进程中的资源。从JVM的方面来说,线程是进程中的一个组件,是执行java代码的最小单位。

在Android应用开发过程中,如果需要处理异步或并发任务时可以使用线程池,使用线程池可以有以下好处:
1、降低资源消耗:线程的创建和销毁都需要消耗资源,重复利用线程可以避免过度的消耗资源。
2、提高响应速度:当有任务需要执行时,可以不用重新创建线程就能开始执行任务。
3、提高线程的管理性:过多的创建线程会降低系统的稳定性,使用线程池可以统一分配,调优和监控。

Thread Pool模式的原理是使用队列对待处理的任务进行缓存,并复用一定数量的工作者线程从队列中取出任务来执行。其本质是使用有限的资源来处理无限的任务。

Thread Pool模式最核心的类是ThreadPoolExecutor,它是线程池的实现类。使用Executors可以创建三种类型的ThreadPoolExecutor类,主要是以下三种类型:
(1)FixedThreadPool
(2)SingleThreadExecutor
(3)CachedThreadPool

三、Executor框架分析

Executor接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。

Executor框架中主要包括:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runable接口、Callable接口和Executors。

下图是Executor框架的类图。
Android多线程编程之线程池学习篇(一)_第1张图片

==首先主线程创建任务:==任务的对象可以通过实现Runnable接口或者Callable接口实现。而Runnable可以通过Executors.callable(Runnable runnable)或者Executors.callable(Runnable runnable, Object result)方法来转换封装为Callable对象。

==其后是执行任务:==执行任务的方式有两种,一种是execut()方法,执行提交的Runnable对象,ExecutorService.execute(Runnable runnable);另外一种是submit()方法,可以执行提交的Runnable对象,ExecutorService.submit(Runnable runnable),或者是执行提交的Callable对象,ExecutorService.submit(Callable callable)。

==取消任务:==可以选择使用FetureTask.cancel(boolean flag)取消任务。

==关闭 ExecutorService:==这将导致其拒绝新任务。有两种方式来关闭 ExecutorService。shutdown() 方法在终止前允许执行以前提交的任务,而 shutdownNow() 方法阻止等待任务启动并试图停止当前正在执行的任务。在终止时,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。

注意:应该关闭未使用的 ExecutorService 以允许回收其资源。

下列方法分两个阶段关闭 ExecutorService。第一阶段调用 shutdown 拒绝传入任务,然后调用 shutdownNow(如有必要)取消所有遗留的任务:

void shutdownAndAwaitTermination(ExecutorService pool) {
   pool.shutdown(); // Disable new tasks from being submitted
   try {
     // Wait a while for existing tasks to terminate
     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
       pool.shutdownNow(); // Cancel currently executing tasks
       // Wait a while for tasks to respond to being cancelled
       if (!pool.awaitTermination(60, TimeUnit.SECONDS))
           System.err.println("Pool did not terminate");
     }
   } catch (InterruptedException ie) {
     // (Re-)Cancel if current thread also interrupted
     pool.shutdownNow();
     // Preserve interrupt status
     Thread.currentThread().interrupt();
   }
 }

四、ThreadPoolExecutor原理分析

当向一个线程池中添加一个任务时,线程池是如何工作的?下面根据源代码来分析其中的原理:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    //如果线程数大于等于基本线程数或者线程创建失败,则将当前任务放到工作队列当中。
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }

    //如果线程池不处于运行中或者任务无法放入队列中,并且当前线程数量小于最大允许的线程数量。则会创建一个线程来执行该任务。
    else if (!addWorker(command, false))
    //抛出RejectExecutionException异常。
        reject(command);
}

以下是线程池的主要处理流程图:

Android多线程编程之线程池学习篇(一)_第2张图片
从图中可以看出,当提交一个任务时,
* 首先判断核心线程池是否都在执行任务,如果还有未执行任务的线程,则会新创建一个核心线程来执行此任务,否则,将进入下一个流程当中。
* 如果存储任务的队列没有满,那么任务则会存储到这个队列当中,如果该队列已经满了,则会进入下一个流程当中。
* 判断线程池当中是否还有非核心线程没有处于工作状态,如果没有,则会创建一个新线程来执行任务,如果已经满了,则会进入下一个阶段来处理。
* 当线程和队列都已经满了的时候,由RejectdeExecutionHandler来处理,处理的方式有四种,如下图所示:

Android多线程编程之线程池学习篇(一)_第3张图片

任务的处理走向从上面这个图就很明了了。

大概了解了ThreadPoolExecutor之后,再来说说如何创建一个线程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);

1)corePoolSize(线程池的基本大小,也可以说是核心线程数的大小):如果提交一个任务时,线程池中的核心线程数小于这个基本大小值,或者是存在核心线程处于空闲状态,则会创建一个线程来执行提交的任务。当线程数量等于基本大小时就不会创建了。
==注意:==当调用了线程池中的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程,即所谓的核心线程。

2)maximumPoolSize(线程池的最大数量):即所谓的线程池所能创建的最大线程数量,这里的数量中包含核心线程和非核心线程。如果队列中的任务存满,另外线程数小于线程池的最大数量,那么会新创建线程类执行任务。
==注意:==当队列是无界队列时,则设置线程池的最大数量值就无效了。

3)keepAliveTime(线程活动保持的时间):线程池的工作线程空闲后,保持存活的时间。
使用场景:当提交的任务过多时,可以设置较大的时间值,充分提高线程的利用率。

4)unit(线程活动保持的时间单位):可以选择相应的时间单位。如天、时、分、秒、毫秒、微秒、千分之一毫秒和纳秒。

5)workQueue(存储任务的队列):用于保存等待的任务的阻塞队列。这种形式的阻塞队列常见一下几种:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出原则排序。
LinkedBlockingQueue:基于链表结构的阻塞队列,按先进先出原则排序。
SynchronousQueue:不存储元素的阻塞队列。
PriorityBlockingQueue:具有优先级的无限阻塞队列。

6)threadFactory(用于创建线程的工厂):通过工厂模式来创建线程。

7)defaultHandler(饱和策略的处理模式):当队列和线程池都满的时候,这种状态就是处于饱和状态了,那么必须采取一种策略来处理不能执行的新任务。关于饱和策略的处理有四种方式:

  • AbortPolicy:直接抛出异常。
  • CallerRunsPolicy:只用调用者所在线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:直接丢弃任务。

五、FixedThreadPool原理分析

FixedThreadPool通过Executors中的newFixedThreadPool()方法来创建的。
这是创建一个可重用固定线程数量的线程池,以共享的无界队列方式来运行这些线程。在这个线程池中只有核心线程,不存在非核心线程,当核心线程处于空闲状态时,线程不会被回收,只有当线程池关闭时才会回收。

(1)如果运行的线程数少于corePoolSize,则会创建新的线程俩执行任务,当有任务来不及给线程来处理时,则会将任务添加到任务队列中。当任务数少于线程数的时候,线程池执行结果如下:

public static void main(String[] args) {
        ExecutorService eService = Executors.newFixedThreadPool(40);
        for (int i = 0; i < 2; i++) {
            final int j = i;
            eService.execute(new Runnable() {

                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " " + j);
                }
            });
        }
    }

运行结果:
pool-1-thread-2 1
pool-1-thread-1 0
说明线程池中先只创建两个线程。

(2)如果任务数量比较多的时候,超过核心线程数,那么当线程执行完任务后会从队列中取任务执行。实例代码如下:

public static void main(String[] args) {
        ExecutorService eService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 2000; i++) {
            final int j = i;
            eService.execute(new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " " + j);
                }
            });
        }
    }

运行结果:
pool-1-thread-1 0
pool-1-thread-4 3
pool-1-thread-3 2
pool-1-thread-2 1
pool-1-thread-4 4
pool-1-thread-3 5
pool-1-thread-2 7
pool-1-thread-1 6
pool-1-thread-4 8
pool-1-thread-3 9
pool-1-thread-2 10
pool-1-thread-1 11
pool-1-thread-4 12

从运行结果可以知道,线程池中的线程不会无限创建,数量最多为corePoolSize大小。

六、SingleThreadExecutor原理分析

SingleThreadExecutor是通过使用Executors的newSingleThreadExecutor方法来创建的,以无界队列方式来运行该线程。这个线程池中内部只有一个核心线程,而且没有非核心线程。SingleThreadExecutor的源代码实现如下:

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

其中corePoolSize和maximumPoolSize被设置为1,其它的与FixPoolThread相同。

(1)当线程池中的线程数少于corePoolSize,则会创建一个新线程来执行任务。

(2)如果任务数量比较多的时候,超过核心线程数,那么当线程执行完任务后会从队列中取任务执行,并且按照顺序执行。实例代码如下:

public static void main(String[] args) {
        ExecutorService eService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 20; i++) {
            final int j = i;
            eService.execute(new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " " + j);
                }
            });
        }
    }

运行结果:
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9
pool-1-thread-1 10
pool-1-thread-1 11
pool-1-thread-1 12
pool-1-thread-1 13
pool-1-thread-1 14
pool-1-thread-1 15
pool-1-thread-1 16
pool-1-thread-1 17
pool-1-thread-1 18
pool-1-thread-1 19
pool-1-thread-1 20

从运行结果可以看出,线程池中只有一个核心线程,另外按照一定的顺序执行任务。

七、CachedThreadPool原理分析

CachedThreadPool是使用Executors中的newCachedThreadPool()方法创建,它是一种线程数量不固定的线程池,没有核心线程,只有非核心线程,非核心线程的数量值为Integer.MAX_VALUE。
CachedThreadPool创建的源代码为:

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

keepAliveTime为60L,说明空闲线程等待新任务id最长时间为60s,如果超过了60s,则该线程会终止,从而被回收。CachedThreadPool使用的队列为SynchronousQueue,这是一个无界队列。

(1)如果线程池中的线程处理任务的速度小于提交任务的速度,那么线程池会不断的创建新线程来处理任务,那么过多的创建线程会耗尽CPU和内存资源。
(2)当线程池中的线程都处于活动状态时,线程池会创建新的线程来处理主线程提交的任务,如果有空闲线程,则会利用空闲线程来处理任务。
(3)SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另外一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。

八、总结

熟悉了线程池的一些相关知识后,就要熟悉如何来合理配置线程池,如何选择哪种方式的线程池。

如何合理配置线程池,就需要从任务的几个方面来分析:

  • 任务的性质:CPU密集型、IO密集型和混合型。
  • 任务的优先级:高、中和低。
  • 任务的执行的时间:长、中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

从任务的性质来说,如果是CPU密集型的任务,那么尽可能的配置少的线程数量,例如配置N+1(N为CPU的核数)个线程的线程池。如果是IO密集型的任务,那么尽可能配置多的线程数,例如2*N。对于混合型的任务,如果可拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。 可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备 的CPU个数。

从任务的优先级来说,可以使用优先级队PriorityBlockingQueue处理。优先级高的任务会先处理,但是这样带来一种不太好的情况就是,优先级低的任务可能一直得不到处理。

从执行时间不同来说,可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。

从依赖性来说,这个主要介绍数据库的连接,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

以上三种方式的线程池,FixedThreadPool适合比较耗资源的任务,SingleThreadExecutor适合按照顺序执行的任务,CachedThreadPool适合执行大量耗时较少的任务。

参考资料:
《Java并发编程的艺术》
《Java多线程编程核心技术》

你可能感兴趣的:(Android,Android多线程)