Android线程池剖析(ThreadPoolExecutor/Executors)

不管是在工作中还是面试中.掌握线程池对我们的帮助都非常大
使用线程池的好处:减少了每个任务的调用开销,当有大量异步任务时能提高性能,更容易管理线程,比如已完成任务的数量
使用线程池的坏处:暂时没发现

会介绍到的内容:

  • ThreadPoolExecutor核心参数详解
  • Executors核心方法详解
  • 如何创建线程(线程工厂)
  • 任务的排队机制
  • 队列的三种策略
  • 拒绝策略
  • Hook methods
  • shutdown和shutdownNow的区别
  • 核心线程数究竟开启多少合适?

ThreadPoolExecutor:
线程池的实现类,通过ThreadPoolExecutor的构造器参数我们可以定义很灵活且适合应用场景的线程池

Executors:
线程池工具类,比较常用的是他提供的几个内置线程池的方法: (newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool),但我们点进去就可以看到他其实也就是在ThreadPoolExecutor上再封装一层而已,将创建线程池变的更灵活更简单

重点就是看下ThreadPoolExecutor如何创建线程池的,他的所有参数定义,再回头看一下Executors创建的四种线程池就比较简单了

首先看下他的构造器:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue 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:核心线程数
  • maximumPoolSize:最大的 线程池最大可以同时运行多少个线程
  • keepAliveTime:非核心线程,那么是有时间限制存在的,将在这个时间终止之前等待新任务
  • unit:keepAliveTime参数的时间单位,比如分钟/秒等(TimeUnit.HOURS)
  • workQueue:线程的队列 在最大线程数之外的任务会放在该队列中维护
  • threadFactory:创建线程的工厂,defaultThreadFactory(默认线程工厂),
  • handler:因为达到了线程边界和队列容量,新的任务被阻止时使用的策略,比如抛出异常,比如静默丢弃任务

核心线程仅在新任务到达时才会创建和启动,当然你可以预启动核心线程, prestartCoreThread 方法启动一个核心线程,如果所有核心线程都已启动该方法返回false当提交新任务时execute(Runnable),运行的核心线程数少于设置的核心线程总数时,即使其他线程处于空闲状态也会创建一个新的线程来处理该任务,


创建线程(threadFactory):

  • Executors.defaultThreadFactory() :使用了1级关联的ThreadGroup线程组,可以有效的对线程进行组织管理,并统一的设置了线程名,优先级(NORM_PRIORITY),非守护线程,我们可以通过实现ThreadFactory就可以自由定义这些属性,这里我举个AsyncTask源码中的自定义ThreadFactory
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());
        }
    };

还有一个我自己定义的线程工厂,我改变了系统默认的优先级

public class TestDefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private static final String TAG = "duo_shine";
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    TestDefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                poolNumber.getAndIncrement() +
                "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon())
            t.setDaemon(false);
//        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.MAX_PRIORITY);
        return t;
    }
}
  • Executors.privilegedThreadFactory() :继承自DefaultThreadFactory,他们之间的区别是在返回的Thread中PrivilegedThreadFactory的newThread中对Worker这个任务进行了再包装,这个工厂捕获访问控制上下文和类加载器。但已经不推荐使用,这个我们使用defaultThreadFactory即可.

任务的排队机制(注意排队机制非一成不变,他会受队列的策略影响):

  • 添加新任务时,如果当前运行的核心线程数小于设置的核心线程最大数,则会新建线程
  • 如果核心线程在运行,任务则更倾向于排队而非使用非核心线程(这点本人已验证)
  • 如果无法排队(队列已满),则会创建新线程,除非他超过maximumPoolSize,这种情况下执行拒绝策略handler

队列的三种策略:(workQueue)

  • 1.SynchronousQueue(直接交接):每个插入操作必须等待另一个线程的相应移除操作
    ,反之亦然,同步队列没有任何内部容量,甚至没有容量,这种机制将任务交给线程而非去保存他们,如果没有线程立即可用于运行它,则会进行排队,并失败,此时如果未达到maximumPoolSizes,则会构建新线程,反之则会执行拒绝策略handler,所以此策略通常需要无限制的maximumPoolSizes以避免拒绝新提交的任务,这个就视自己的情况而定了,注意此时排队机制就有所改变,第二条中任务不再倾向于排队,因为该队列内部无容量在上面已经说了,当我使用getQueue测试时他的容量是0size,所以它的排队机制是当新任务到来时核心线程如果数量未达到核心线程总数,则创建或直接使用核心线程,同样的如果核心线程都在工作再创建或直接使用非核心线程
  • 2.LinkedBlockingQueue(无限队列):指的是LinkedBlockingQueue无参构造创建的对象,他的默认容量是Integer.MAX_VALUE,这将导致除核心线程之外的任务都将进入队列中等待核心线程,因此只会创建核心线程,这也变相导致了maximumPoolSize的值是没有什么影响的根据上面的任务的排队机制的第二条我们知道,这种策略更适用于每个任务都独立于其他任务,且耗时不应该太久,比如你的任务是做死循环,那么此核心线程无法结束任务,导致工作队列无线增长
  • 3.ArrayBlockingQueue(有限队列):个人觉得此策略和LinkedBlockingQueue有参构造区别不大,同样的指定容量,同样的优先核心线程,同样的排队优先于使用非核心线程

拒绝策略:(handler)

拒绝策略什么情况下触发?
当Executor关闭时,以及当任务超过maximumPoolSize和队列的最大容时,execute(Runnable)将拒绝在方法中提交的新任务,会在execute中调用reject方法,该方法内部就是调用了handler.rejectedExecution(command, this);此handler是RejectedExecutionHandler类型,他提供了四种预定义的策略:

  1. ThreadPoolExecutor.AbortPolicy:拒绝时抛出异常
  2. ThreadPoolExecutor.CallerRunsPolicy:调用execute自身的线程运行任务,使用这个策略一定要注意,如果你在主线程调用的,并且触发了拒绝策略且你的任务是耗时操作,那么会阻塞main线程
  3. ThreadPoolExecutor.DiscardPolicy:删除无法执行的任务,他做了空实现,任务直接被丢弃
  4. ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序未关闭,则删除工作队列头部的任务,然后重试执行(可能会失败,导致重复执行),如果执行线程已关闭则任务丢弃

Hook methods:

  • beforeExecute():任务执行前
  • afterExecute():任务执行后
  • terminated():当Executor关闭时(shutdown()/shutdownNow()调用时)

我们可以通过重写这些方法来操作任务的执行环境,例如,重新初始化ThreadLocals,收集统计信息或添加日志条目,此外还可以在terminated()方法中做Executor完全关闭后需要执行的任何特殊处理

比如下面这段代码:

/**
 * Created by duo_shine on 2018
 */
public class TestThreadPoolExecutor extends ThreadPoolExecutor {
    private static final String TAG = "duo_shine";

    public TestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public TestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public TestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public TestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    /**
     * 任务执行前
     * @param t
     * @param r
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        boolean mainThread = isMainThread();
        Log.d(TAG, "beforeExecute:" + mainThread);
        Log.d(TAG, "beforeExecute:任务即将开始执行 : ");
    }

    /**
     * 任务执行后
     * @param r
     * @param t
     */
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        boolean mainThread = isMainThread();
        Log.d(TAG, "afterExecute:" + mainThread);
        Log.d(TAG, "afterExecute:任务执行结束 : ");
    }

    @Override
    protected void terminated() {
        super.terminated();
        Log.d(TAG, "terminated:Executor关闭 : ");
    }

    public boolean isMainThread() {
        return Looper.getMainLooper().getThread().getId() == Thread.currentThread().getId();
    }
}

这里写图片描述


shutdown和shutdownNow的区别
1.shutdown:其中先前提交的任务将被执行,但不会接受任何新任务。注意如果已经调用shutdown后再调用execute比如这样

mTpe.execute(runnable);
mTpe.shutdown();
mTpe.execute(runnable);

会直接走拒绝策略handler这是一个需要注意的点,如果你的拒绝策略是ThreadPoolExecutor.AbortPolicy()那就直接凉凉了

2.shutdownNow:尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。注意如果我们在做一个死循环任务,那么这个线程通过interrupt是停止不了的,具体可以看我另一篇博客interrupt能停止线程吗


Note: ThreadPoolExecutor的所有构造函数中的参数不可以传负数和null,否则将直接抛出异常


番外篇:
1.核心线程设置为0的情况:
在LinkedBlockingQueue策略下只会创建一个非核心线程来执行任务,这个线程在new Worker时传入null来创建一个线程(正常创建线程使用任务来创建一个新线程)
2.核心线程数究竟开启多少合适(需要考虑手机的性能):这里参照AsyncTask源码中的写法

 private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

官方的意思是希望核心池中至少有2个线程和最多4个线程,更喜欢比CPU数少1,以避免饱和


Executors: Executors中提供了一些快速创建线程池的方法,我们简单学习几个

Executors.newFixedThreadPool,看下他的构造器

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

分析一波…
nThreads用做核心参数和最大线程数,队列采用无界队列,也就是说如果使用这个线程池,比如我们传入5,那么该线程池最多同时只有5个线程同时执行任务,更多的任务将被放入无界队列中等待核心线程来执行,且线程在创建后如果没有更多后续任务不会自动销毁,除非shutdown


Executors.newSingleThreadExecutor

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

分析一波…
这里更简单了,该线程池最大只能有一个线程工作,且为核心线程,和newFixedThreadPool的区别在对任务的处理上没啥区别,但注意返回值类型不同,此线程池返回值只能使用ExecutorService,不可以重新配置了,但是newFixedThreadPool可以使用ThreadPoolExecutor作为返回的对象


Executors.newCachedThreadPool:

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

分析一波…
核心线程数为0,而最大线程数是无界的,我们只需要注意他使用的队列策略,SynchronousQueue在上面已经分析过了,他不会存储任务,该线程池的工作机制是如果有空闲线程则使用空闲线程执行任务,如果没有空闲线程则创建新线程,注意无核心线程所以他的非核心线程的空闲等待时间是60秒,如果60秒线程没有接收到任务就销毁以回收资源,该线程池适用于大量短时任务


简单介绍了几个系统内置的线程池创建方法,当你熟悉ThreadPoolExecutor的使用时,我们可以自己构建适合自己业务场景的线程池


本来理论我都对应着日志或代码.但由于导致篇幅太长,所以只精简下来这些,有错请指出好以改正

你可能感兴趣的:(Android)