【性能优化】线程池真的很重要

3,636 阅读10分钟

Excutor框架

ThreadPoolExcutor是核心的类

主要参数

  • corePoolSize: 线程池的核心线程数,默认条件下,核心线程会一直存活在线程池中,即便核心线程处于空闲状态。 可以设置AllowCoreThreadTimeOut为ture,当核心线程空闲时间超过keepAliveTime,便会被销毁

  • maximumPoolSize: 线程池中最大的线程数

  • keepAliveTime: 线程池允许非核心线程空闲的时间,可通过设置AllowCoreThreadTimeOut控制核心线程

  • unit:keepAliveTime的单位

  • workQueue: 线程池的缓冲队列,用来存放等到执行的任务

  • handler:当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是AbortPolicy,该策略保证在线程池满的情况下任何试图提交任务到该线程池的线程的线程均会抛出RejectedExecutionException。

线程池的状态

关于线程池的状态,有5种:

  • RUNNING, 运行状态,值也是最小的,刚创建的线程池就是此状态。
  • SHUTDOWN,停工状态,不再接收新任务,已经接收的会继续执行
  • STOP,停止状态,不再接收新任务,已经接收正在执行的,也会中断
  • TIDYING,清空状态,所有任务都停止了,工作的线程也全部结束了
  • TERMINATED,终止状态,线程池已销毁

它们的流转关系如下:

线程池任务执行

1. 添加任务

  • execute():没有返回值
  • submit(): 返回future对象,可通过get()方法获取返回值,或者cancel()方法取消任务

2. 任务提交过程

  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

  3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

总结即:处理任务判断的优先级为 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

3. 任务停止

  • shutdown: 不添加新任务,已经正在执行的任务不会立即停止执行
  • shutdownnow: 不添加新任务,中断正在执行的任务

常用阻塞队列

  • ArrayBlockingQueue: 数组实现的容量固定的有界阻塞队列
  • LinkedBlockingQueue: 单链表实现的阻塞队列,可以设置容量大小,如果不设置,容量默认为Integer.MAX_VALUE,无界阻塞队列
  • SynchronousQueue: 没有容量的,不能存储数据的队列,一个线程的put必须等待另一个线程的take,offer()的时候如果没有另一个线程在poll()或者take()的话返回false

常见的线程池

executors提供了工厂方法来创建几种常见的线程池

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

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

BlockingQueue选择了LinkedBlockingQueue,该queue有一个特点,他是无界的。 由于是无界的,所以队列就不会满,当线程数等于核心线程后,也不会创建非核心线程。这也是newSingleThreadExecutor为单线程的原因

  • newFixedThreadPool(int nThreads) 固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS,  
                                      new LinkedBlockingQueue<Runnable>());  
    }  

和newSingleThreadPool类似,只不过是固定大小线程,不是单线程

corePoolSize和maximumPoolSize的大小是一样的(实际上,如果使用无界queue的话maximumPoolSize参数是没有意义的)

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

首先是无界的线程池,所以我们可以发现maximumPoolSize为非常大。其次BlockingQueue的选择上使用 SynchronousQueue , SynchronousQueue 它将任务直接提交给线程而不保持它们,在此,如果不存在可用于立即运行任务的线程 ,则试图把任务加入队列将失败,因此会构造一个新的线程 。 该线程池非常适用于短时间内大量的轻量级的任务

  • newScheduledThreadPool(int) 延时或周期性任务
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

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

Rxjava中就是使用的该线程池。创建一个定长线程池,支持定时及周期性任务执行。阻塞队列为DelayedWorkQueue。

注意点

阿里巴巴Java开发手册中对线程池的使用规范中要求线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明: Executors 返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。

线程是一种系统资源,本身创建就会带来内存开销,同时操作系统对单进程可创建的线程数也是有限制的。

在 Android 中,每个线程初始化都需要 mmap 一定的堆内存,在默认的情况下,初始化一个线程大约需要 mmap 1MB 左右的内存空间。同时系统本身也会对每个进程可创建的线程数,做一定的限制,这个限制在 /proc/pid/limits 中,不同的厂商对这个限制也有所不同,当超出限制时,哪怕堆上还有可用内存,依然会抛出 OOM。

线程池在开源库中的使用

1.在AsyncTask中使用

 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));//核心线程数2~4
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;//最大线程数
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);// 任务队列设置大小

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR;
static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

  @MainThread
    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);// 默认使用SerialExecutor
    }

SerialExecutor 创建了一个ArrayDeque双端队列,保证任务串行,也可以使用THREAD_POOL_EXECUTOR,实现并发执行

2.在okhttp中使用

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(
      //corePoolSize 为 0表示,没有核心线程,所有执行请求的线程,使用完了如果过期了。(keepAliveTime)就回收了  
      //maximumPoolSize 无限大的线程池空间
          0, 
          Integer.MAX_VALUE, 
          60, 
          TimeUnit.SECONDS,
          // workQueue 通过execute方法发送的任务,会先被缓存在这个队列中
          // SynchronousQueue是不存储数据的阻塞队列
          new SynchronousQueue<Runnable>(), 
          Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

可以看出,在Okhttp中,构建了一个阀值为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列,一个叫做"OkHttp Dispatcher"的线程工厂。

也就是说,在实际运行中,当收到10个并发请求时,线程池会创建十个线程,当工作完成后,线程池会在60s后相继关闭所有线程。

3.在WorkManager中的使用

 private final ExecutorService mBackgroundExecutor =
            Executors.newSingleThreadExecutor(mBackgroundThreadFactory);

单线程的线程池保证任务的顺序执行

源码分析

JDK中线程池的核心实现类是ThreadPoolExecutor,先看这个类的第一个成员变量ctl,AtomicInteger这个类可以通过CAS达到无锁并发,效率比较高,这个变量有双重身份,它的高三位表示线程池的状态,低29位表示线程池中现有的线程数,这也是Doug Lea一个天才的设计,用最少的变量来减少锁竞争,提高并发效率。

    //CAS,无锁并发
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //表示线程池线程数的bit数, 29
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //最大的线程数量,数量是完全够用了
    // 00011111111111111111111111111111 (29个1)
    private static final int CAPACITY = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    //1110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int RUNNING = -1 << COUNT_BITS;
    //0000 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int SHUTDOWN = 0 << COUNT_BITS;
    //0010 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int STOP = 1 << COUNT_BITS;
    //0100 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TIDYING = 2 << COUNT_BITS;
    //0110 0000 0000 0000 0000 0000 0000 0000(很耿直的我)
    private static final int TERMINATED = 3 << COUNT_BITS;

    // Packing and unpacking ctl
    //获取线程池的状态
    private static int runStateOf(int c) { return c & ~CAPACITY; }
    //获取线程的数量
    private static int workerCountOf(int c) { return c & CAPACITY; }
    //组装状态和数量,成为ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    /*
     * Bit field accessors that don't require unpacking ctl.
     * These depend on the bit layout and on workerCount being never negative.
     * 判断状态c是否比s小,下面会给出状态流转图
     */
    
    private static boolean runStateLessThan(int c, int s) {
        return c < s;
    }
    
    //判断状态c是否不小于状态s
    private static boolean runStateAtLeast(int c, int s) {
        return c >= s;
    }
    //判断线程是否在运行
    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }

重点看execute的实现

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task. The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread. If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        // 小于核心线程数,则添加线程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
       // 检查线程池是否是运行状态,然后将任务添加到等待队列,如果队列已满,则添加失败返回false
        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))
            reject(command);
    }

线程池

线程池ThreadPoolExecutor

彻底理解Java线程池原理篇

被开发者抛弃的 Executors,错在哪儿?