初探Android线程池

1,295 阅读8分钟

前言

最近在看OkHttp的源码,看的时候发现有关线程池的运用,自己就仔细想了一下,这个块知识好像不是很牢固。没办法,再研究一下有关线程池的相关知识吧。学习就是一个查漏补缺的过程,最终的目的还是要形成自己的知识网络。

为什么要使用线程池

平时在Android开发的过程中经常会用到多线程异步处理相关任务,每开一个线程都要新建一个Thread对象来处理,这种操作会造成哪些后果呢?

1、系统执行多任务时,会为每个任务创建对应的线程,当任务执行结束之后会销毁对应的线程,在这种情况下对象被频繁的创建和销毁。
2、当对线程象被频繁时会占用大量的系统资源,在并发的过程中会造成资源竞争出现问题。大量的创建线程还会造成混乱,没有一个统一的管理机制,容易造成应用卡顿。
3、大量线程对象被频繁销毁,将会频繁出发GC机制,从而降低性能。

由于多线程异步处理任务有可能造成这样或者那样的问题,那么线程池应运而生。

线程池的作用

我们来看一下使用线程池的好处:

1、重用线程池中的线程,避免因频繁创建和销毁线程造成的性能消耗。
2、更加有效的控制线程的最大并发数,防止线程过多抢占资源造成的系统阻塞。
3、对线程进行有效的管理。

线程池类继承结构

我们先看一张有关线程池的类继承结构图:

Excutor:
 这只是一个接口,其中只定义了一个execute(Runnable command)方法,从接收参数来看只能执行Runnable任务,不能执行Callable带有的返回值的任务。

ExecutorService:
 这也是一个接口,继承于Excutor。但是它Excutor的基础上添加了管理线程池生命周期的方法shutdown()shutdownNow()。同时,ExecutorService还支执行Callable带有返回值的任务。当提交完任务之后会拿到一个Future返回值,这个返回值代表了任务执行完毕的结果。
shutdown()会等之前的任务执行完毕之后在关闭,同时也不会再接收新任务。如果我们需要等待线程池处理完成再返回,可以使用awaitTermination方法来等待完成。
shutdownNow()方法会尝试马上关闭所有正在执行的任务,并且跳过所有已经提交但是还没有运行的任务。但是对于正在执行的任务,是否能够成功关闭它是无法保证的,有可能他们真的被关闭掉了,也有可能它会一直执行到任务结束。

ThreadPoolExecutor:
 这个类线程池的核心类,我们这篇文章主要来分析它。

ThreadPoolExecutor

构造函数

这个类中有很多构造函数,我们找一个参数最多的构造函数来看一下。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

corePoolSize

这个参数表示的是核型线程数,当一个请求进来时当前线程池中的线程个数小于核心线程数,可以直接通过ThreadFactory创建线程池;如果已经大于核心线程数时,则将任务放入到workQueue(任务队列)中。

maximumPoolSize

这个参数表示线程池中可以创建的最大线程数。当线程池中的线程数等于corePoolSize并且workQueue(任务队列)已满,这时就要看当前线程数是否大于maximumPoolSize,如果小于则会创建线程去执行任务,否则会时候“饱和策略”去拒绝这个任务请求。对于超过corePoolSize的线程称之为“idle Thread”,这部分线程会有一个最大的空闲存活时间,如果超过这个空闲存活时间还没有任务被分配,则会将这些线程进行回收。

keepAliveTime 和 unit

这两个参数就是用来控制“idle Thread”(空闲线程)的空闲存活时间,u nit表示时间单位,当超过这个时间时将会被回收。在ThreadPoolExecutor中有一个非常重要参数private volatile boolean allowCoreThreadTimeOut,这个参数表示当核心线程超过最大空闲时间还没被分配任务是是否回收,默认返回false,表示不会被回收。如果将这个参数设置成true的话,当核心线程超过最大空闲时间时将会被回收。

workQueue

阻塞队列,当线程数超过corePoolSize的部分任务会被放入到这个队列中等待执行。阻塞队列也会被分成有界和无界,当我们制定这个队列的capacity时,就是一个有界阻塞队列,反之就是一个无界阻塞队列。当该队列为无界阻塞队列时,会有大量的任务被存入,从而导致内存溢出系统崩溃。

threadFactory

这是一个线程工厂,被用来为线程池创建线程。当我们不指定线程工厂时,线程池内部会调用Executors.defaultThreadFactory()创建默认的线程工厂,其后续创建的线程优先级都是Thread.NORM_PRIORITY。如果我们指定线程工厂,我们可以对产生的线程进行一定的操作。

handler

饱和策略,当线程池达到饱和状态时拒绝多余的任务。ThreadPoolExecutor中有三种饱和策略,AbortPolicy:执行策略时抛出RejectedExecutionException异常。CallerRunsPolicy:不在线程池中运行任务,在调用者的线程中运行任务。DiscardOldestPolicy:将队列中等待最久的直从队列头部移除,将新的任务加入到队列尾部。DiscardPolicy:直接丢弃任务。

执行方法

线程池中有两种执行方法,分别是submit()execute(),下面我们通过源码看一下两者的区别。

execute()

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get(); //1
        if (workerCountOf(c) < corePoolSize) { //2
            if (addWorker(command, true)) //3
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { //4
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command)) //5
                reject(command);
            else if (workerCountOf(recheck) == 0) //6
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) //7
            reject(command);
    }

在上面有几处注释,我们看一下。

1、获取当前线程池的状态和有效线程的个数。
2、判断当前线程池中的线程个数是否小于核心线程数。
3、如若没有超过核心线程数,将直接创建核心线程执行任务,如果创建成功直接返回,如果不成功将进行下一步
4、判断当前线程池的状态是否为RUNNING状态,并将任务添加到队列中。
5、查看一下线程池的状态,如果不是RUNNING,直接移除。
6、如果当前线程池中线程数量为0,则单独创建线程,但是不指定任务。
7、如果上述条件都不瞒住,并且创建一个非核心线程来执行任务失败,直接调用reject方法

submit()

/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
    
/**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

submit(...)方法其实是AbstractExecutorService中的方法,ThreadPoolExecutor继承于它,这里的submit``方法又会调用ThreadPoolExecutorexecute(...)```方法。

线程的池的运行过程

从上面的执行方法我们能获得下面关于线程池运行过程的图。

线程池的运行过程主要分一下几个步骤:

1、当需要执行的任务被提交到线程池后,首先判断当前运行的线程是否少于corePoolSize。如果小于,则创建新线程来执行任务。
2、如果当前线程池中运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
3、BlockingQueue已满则无法将任务加入,这时就会创建新的线程来处理任务。
4、如果创建新线程会让当前运行的线程数超出maximumPoolSize,拒绝任务,并调用RejectedExecutionHandler.rejectedExecution()方法。

文章开头也讲过,最近在看okhttp源码的时候碰到线程池这个不太熟悉的知识点,就赶紧过来研究一下。由于时间仓促,这篇文章有些简短且浅显,有关线程池深入的知识将会在后续文章中展示。

参考资料

线程池原理
Java线程池