Android 线程池的使用

线程池优点
提到线程池就必须先说一下线程池的优点,线程池的优点可以概括为以下四点:
* 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销;
* 线程池旨在线程的复用,就避免了创建线程和销毁线程所带来的时间消耗,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量;
* 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象;
* 能够对线程进行简单的管理,并提供定时执行以及指定时间间隔循环执行等功能。

线程池的创建
Android 中的线程池的概念来源于 Java 中的 Executor ,Executor 是一个接口,真正的线程池的实现为 ThreadPoolExecutor,ThreadPoolExecutor 提供了一些列的参数来配置线程池,通过不同的参数可以创建功能特性不同的线程池。我们要创建一个线程池只需要 new ThreadPoolExecutor(…) 就可以创建一个线程池,但我们如此创建线程池的话,需要配置一些参数,非常麻烦,同时 Google 官方也不推荐使用这种方式来创建线程池,而是推荐使用 Executors 的工厂方法来创建线程池。但 Executors 的工厂方法创建的线程池也是直接或间接通过配置 ThreadPoolExecutor 参数来实现的,因此有必要先了解 ThreadPoolExecutor 的配置参数。

ThreadPoolExecutor
ThreadPoolExecutor 的构造方法提供了一些列的参数来配置线程池,先来了解一下 ThreadPoolExecutor 的构造方法中各个参数的含义,这些参数将直接影响到线程池的功能特性。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize, 
                              long keepAliveTime, 
                              TimeUnit unit, 
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory, 
                              RejectedExecutionHandler handler) {...
    }

corePoolSize 线程池的核心线程数,默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态。但如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true ,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔是由 keepAliveTime 所指定,当等待时间超过 keepAliveTime 所指定的时长后,核心线程就会被终止。
maximumPoolSize 线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。
keepAliveTime 非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true 时,keepAliveTime 同样会作用于核心线程。
unit 用于指定 keepAliveTime 参数的时间单位,这是一个枚举,常用的有 TimeUnit .MILLISECONDS 和 TimeUnit .SECONDS。
workQueue 线程池中的任务队列,通过线程池的 execute 方法提交的 Runnable 对象会存储在这个参数中。
threadFactory 线程工厂,为线程池提供创建新的线程的功能。threadFactory 是一个接口,它只有一个方法: public abstract Thread newThread (Runnable r);
RejectedExecutionHandler 通常叫做拒绝策略,在线程池已经关闭的情况下 ,或者任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用 execute() 来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个 RejectedExecutionException 异常。

ThreadPoolExecutor 执行任务时策略
以下用 currentSize 表示线程池中当前线程数量
1. 当 currentSize < corePoolSize 时,将会直接启动一个核心线程来执行任务。
2. 当 currentSize >= corePoolSize ,那么任务会被插入到任务队列中排队等待执行。
3. 如果任务队列已满,但 currentSize < maximumPoolSize,那么会立刻启动一个非核心线程来执行任务
4. 如果任务队列已满,并且 currentSize > maximumPoolSize,那么就拒绝执行任务,ThreadPoolExecutor 会调用 RejectedExecutionHandler 的 rejectedExecution 方法来通知调用者。
下面通过 demo 来验证一下:

 ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(128));
        for (int i = 0; i < 30; i++) {
            final int index = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
        }

在示例中,核心线程数为 3,workQueue 的大小为 128,所以我们的线程的执行应该是先启动三个核心线程来执行任务,剩余的 27 个任务全部存在workQueue 中,等待核心线程空余出来之后执行。
这里写图片描述

那我把构造 ThreadPoolExecutor 的参数修改一下,来验证一下我们上面的结论正确与否。

 ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 30, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(6));
        for (int i = 0; i < 30; i++) {
            final int index = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        String threadName = Thread.currentThread().getName();
                        Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
        }

这里写图片描述
首先打印出来 0,1,2 说明往核心线程添加了三个任务,然后将 3,4,5,6,7,8 六个任务添加到了任务队列中,接下来要添加的任务因为核心线程已满,队列已满所以就直接开一个非核心线程来执行,因此后添加的任务反而会先执行(3,4,5,6,7,8都在队列中等着),所以我们看到的打印结果是先是 0~2,然后 9~29,然后 3~8,当然,我们在实际开发中不会这样来配置最大线程数和线程队列。

线程池的分类
由于 Google 官方不推荐使用 new 的方式来创建线程池,而是推荐使用 Executors 的工厂方法来创建线程池,通过直接或间接的配置 ThreadPoolExecutor 的参数来构建线程池,常用的线程池有如下 4 种,newFixedThreadPool ,newCachedThreadPool,
newScheduledThreadPool 和 newSingleThreadExecutor。

FixedThreadPool 通过 Executors .newFixedThreadPool 来创建。它是一个线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了。当所有的线程都处于活动状态时,新任务都会处于等待状态,直到有线程空闲出来。由于 FixedThreadPool 只有核心线程并且这些核心线程不会被回收,这意味着它能快速的响应外界的请求。 newFixedThreadPool 方法的实现如下,可以发现 FixedThreadPool 中只有核心线程并且这些核心线程没有超时机制,另外任务队列也是没有大小限制。

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

CachedThreadPool 通过 Executors.newCachedThreadPool 来创建,它是一个线程数量不定的线程池,它只有非核心线程,并且最大线程数为 Integer.MAX_VALUE ,由于 Integer.MAX_VALUE 是一个很大的数,实际上就相当于最大核心线程数可以任意大。当线程池中的线程都处于活动状态时,当有新任务过来时,线程池就会创建新的线程来处理新任务,否则就会利用空闲的线程来处理。线程池中的空闲线程都有超时机制,这个超时时长为 60 秒,超过 60 秒闲置线程就会被回收。SynchronousQueue 队列相当于一个空队列,这就导致任何任务都会立即执行。

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

ScheduledThreadPool 通过 Executors.newScheduledThreadPool 来创建,它的核心数量是固定的,非核心线程没有限制,并且当非核心线程闲置时会被立即回收,ScheduledThreadPool 这类线程池主要用于执行定时任务和具有固定周期的重复任务。

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

SingleThreadExecutor 通过 Executors.newSingleThreadExecutor 来创建。这类线程池内部只有一个核心线程,它确保所有的任务都在同一个线程中按顺序执行。SingleThreadExecutor 的意义在于统一所有的外界任务到一个线程中,这使得这些任务之间不需要处理线程同步的问题。

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

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

execute(Runnable command)
Executes the given task sometime in the future.

该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由 ThreadPoolExecutor 的实现决定。

newFixedThreadPool 创建一个固定线程数量的线程池,示例为:

 @Override
    public void onClick(View v) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                   Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

在上述代码中,创建了线程数量固定为 3 的线程池,该线程池支持的最大线程也为 3 ,而我们创建了 10 个任务让它处理,通过 log 分析,执行的情况是先执行前 3 个任务,后面 7 个任务都进入任务队列进行等待,执行完前 3 个任务后,再通过 FIFO 的方式从任务队列中取任务执行,直到所有任务都执行完毕。
这里写图片描述

newSingleThreadExecutor 创建一个只有一个线程数的线程池。通过 log 分析,每次线程池只执行一个任务,其余的任务都将进入任务队列进行等待。

@Override
    public void onClick(View v) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

代码改动不大,只是改动了 ThreadPoolExecutor 的实现方式,任务都是一个一个的执行,并且都是同一个线程。
这里写图片描述

newCachedThreadPool 创建一个可以根据实际情况调整线程池中线程数量的线程池

 @Override
    public void onClick(View v) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    Log.v(TAG, "线程:"+threadName+",正在执行第" + index + "个任务");
                    try {
                        long time = index * 500;
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

        }
    }

为了体现该线程池可以自动根据实际情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔 1s 去提交一个新任务,这个新任务执行的时间也是动态变化的。
这里写图片描述
通过 log 可以看出,新增的任务都会被立即执行。

newScheduledThreadPool 创建一个可以定时或周期性执行任务的线程池。

@Override
    public void onClick(View v) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);
        threadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                Log.v(TAG, "线程:" + threadName + ",正在执行");
            }
        }, 2, 1, TimeUnit.SECONDS);
    }

延迟 2 秒后,每隔 1 秒执行一次该任务
这里写图片描述
从 log 日志可以看出 ScheduledThreadPool 是 4 个线程池里面唯一一个个有延迟执行和周期重复执行的线程池。

总结
1. FixedThreadPool 只有核心线程,并且数量是固定的,也不会被回收,能更快地响应外界请求。
2. SingleThreadPool 只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。
3. CachedThreadPool 只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程处理任务,任何任务都会被立即执行。
4. ScheduledThreadPool 核心线程数固定,非核心线程数没有限制,主要用于执行定时任务以及有固定周期的重复任务。

好了,关于线程池方面的内容今天就分析到这里,在这篇文章中我们主要学习了线程池的基本概念和基本用法。下一篇文章当中,将会继续带着大家学习线程池的高级用法,讲一讲自定义线程池的知识,敬请期待。
Android 线程池高级使用

猜你喜欢

转载自blog.csdn.net/liqianwei1230/article/details/78239281